Pregunta Buscando un truco para evitar crear un campo en una plantilla de clase


Tengo una clase similar a la protección de alcance (este es el caso de prueba simplificado):

template<void(*close)()>
struct Guard1
{
  template<typename O>
  Guard1(O open) { open(); }

  ~Guard1() { close(); }
};

void close() { std::cout << "close g1\n"; }

int main()
{
  Guard1<close> g1 = [](){ std::cout << "open g1\n"; };
}

Lo modifiqué para que la expresión de cierre también se pueda dar como un lambda:

class Guard2
{
  std::function<void()> close;
public:

  template<typename O, typename C>
  Guard2(O open, C close) : close(close)
  {
    open();
  }

  ~Guard2() { close(); }
};


int main()
{
  Guard2 g2(
      [](){ std::cout << "open g2\n"; },
      [](){ std::cout << "close g2\n"; });
}

Sin embargo, tuve que introducir un campo adicional const std::function<void()>& close; Pasar la lambda del constructor al destructor.

¿Hay alguna manera de evitar este campo adicional mientras se mantiene la lambda (y también una buena sintaxis cuando se usa)?


5
2018-06-18 13:43


origen


Respuestas:


Ya que desea usarlo solo como ScopeGuard, entonces puede estar seguro de que la referencia constante o la referencia de valor a su close() son validos. Necesita un miembro o una clase base como en otra respuesta, pero esta no es una gran diferencia. Pero puede tenerlo como referencia de valor a su lambda, no a std::function que es de un costo de rendimiento bastante grande:

template <class Close>
class ScopeGuard {
public:
    template <typename Open>
    ScopeGuard(Open&& open, Close&& close) 
         : close(std::forward<Close>(close))
    {
        open();
    }
    ScopeGuard(ScopeGuard&& other) : close(std::move(other.close))
    {}
    ~ScopeGuard()
    {
        close();
    }
private:
    Close&& close;
};

Para que sea más fácil de usar, tenga esto make función:

template <class Open, class Close>
auto makeScopeGuard(Open&& open, Close&& close)
{
    return ScopeGuard<Close>(std::forward<Open>(open), 
                             std::forward<Close>(close));
}

Y el uso:

#include <iostream>
using namespace std;

int main() 
{
   int i = 0;
   auto scope = makeScopeGuard([&i]{cout << "Open " << i++ << "\n";}, 
                               [&i]{cout << "Close " << i++ << "\n";});
   cout << "Body\n";
}

Salida:

Open 0
Body
Close 1

Verifiqué que funciona para gcc y clang, C ++ 14 sin errores / advertencias.


4
2018-06-18 15:47



Generalmente no es posible. A diferencia de un puntero de función, un lambda puede capturar y, por lo tanto, contener el estado de tiempo de ejecución. Es cierto que su lambda no lo hace, y por lo tanto puede ser convertido a un puntero de función, pero eso no lo convierte en un argumento de plantilla.


3
2018-06-18 15:08



Si puedes aceptar un poco de trampa: rellena la lambda en una clase base en lugar de un campo:

#include <iostream>

template<typename Base>
struct Guard1 : public Base
{
  template<typename O>
  Guard1(O open, Base base ) : Base(base) { open(); }
  Guard1(Guard1 const& rhs) : Base(static_cast<Base const&>(rhs)) { }

  ~Guard1() { (*this)(); }
};

template<typename O, typename C>
Guard1<C> makeGuard(O o, C c) { return Guard1<C>(o,c); }

int main()
{

  auto g1 = makeGuard([](){ std::cout << "open g1\n"; },
                      [](){ std::cout << "close g1\n"; } );
}

2
2018-06-18 15:19



¿Hay alguna manera de evitar este campo adicional mientras se mantiene la lambda (y también una buena sintaxis cuando se usa)?

Sí: si observa, no hay nada que ganar pasando la función de abrir a su protector de alcance (por lo tanto, el Principio de Responsabilidad Individual establece que no debería tenerlo allí).

También debe pasar la función como un parámetro de tiempo de ejecución, no como un parámetro de plantilla. Esto permitirá una sintaxis más natural en el código del cliente.

Debe hacer que el tipo sea independiente en el tipo de plantilla. Esto también creará una sintaxis más natural en el código del cliente.

Debes asegurarte de que el destructor no tira.

class Guard final
{
public:
    Guard1(std::function<void()> at_scope_exit)
    : f_(std::move(at_scope_exit))
    {
    }

    ~Guard1() noexcept { try{ f_(); } catch(...) {} }
private:
    std::function<void()> f_;
};

Su código de cliente debería verse así:

int x()
{
    operation.begin_transaction();
    Guard commit{ [&operation](){ operation.commit_transaction(); } };

    // do things & stuff here
}

1
2018-06-19 07:43



Tal vez puedas usar la respuesta de esta pregunta. Espero no estar equivocado, pero si fuera posible usar el puntero del constructor, podría pasarlo a type_traits (mira la primera respuesta en esa pregunta), y obtén el segundo argumento que sería función cercana, y luego podrías alias.

Dado que no es posible obtener el puntero del constructor, ¿quizás puede usar otra función miembro para inicializar su objeto?


0
2018-06-18 14:29