vendredi 28 août 2015

Can this technique for creating a container of heterogenous functors be salvaged?

This blog post describes a technique for creating a container of heterogeneous pointers. The basic trick is to create a trivial base class (i.e. no explicit function declarations, no data members, nothing) and a templated derived class for storing std::function<> objects with arbitrary signatures, then make the container hold unique_ptrs to objects of the base class. The code is also available on GitHub.

I don't think this code can be made robust; std::function<> can be created from a lambda, which might include a capture, which might include a by-value copy of a nontrivial object whose destructor must be called. When the Func_t type is deleted by unique_ptr upon removal from the map, only its (trivial) destructor will be called, so the std::function<> objects never get properly deleted.

I've replaced the use-case code from the example on GitHub with a "non-trivial type" that is then captured by value inside a lambda and added to the container. In the code below, the parts copied from the example are noted in comments; everything else is mine. There's probably a simpler demonstration of the problem, but I'm struggling a bit to even get a valid compile out of this thing.

#include <map>
#include <memory>
#include <functional>
#include <typeindex>
#include <iostream>

// COPIED FROM http://ift.tt/1UbH5ke
namespace {

  // The base type that is stored in the collection.
  struct Func_t {};
  // The map that stores the callbacks.
  using callbacks_t = std::map<std::type_index, std::unique_ptr<Func_t>>;
  callbacks_t callbacks;

  // The derived type that represents a callback.
  template<typename ...A>
    struct Cb_t : public Func_t {
      using cb = std::function<void(A...)>;
      cb callback;
      Cb_t(cb p_callback) : callback(p_callback) {}
    };

  // Wrapper function to call the callback stored at the given index with the
  // passed argument.
  template<typename ...A>
    void call(std::type_index index, A&& ... args)
    {
      using func_t = Cb_t<A...>;
      using cb_t = std::function<void(A...)>;
      const Func_t& base = *callbacks[index];
      const cb_t& fun = static_cast<const func_t&>(base).callback;
      fun(std::forward<A>(args)...);
    }

} // end anonymous namespace

// END COPIED CODE

class NontrivialType
{
  public:
    NontrivialType(void)
    {
      std::cout << "NontrivialType{void}" << std::endl;
    }

    NontrivialType(const NontrivialType&)
    {
      std::cout << "NontrivialType{const NontrivialType&}" << std::endl;
    }

    NontrivialType(NontrivialType&&)
    {
      std::cout << "NontrivialType{NontrivialType&&}" << std::endl;
    }


    ~NontrivialType(void)
    {
      std::cout << "Calling the destructor for a NontrivialType!" << std::endl;
    }

    void printSomething(void) const
    {
      std::cout << "Calling NontrivialType::printSomething()!" << std::endl;
    }
};

// COPIED WITH MODIFICATIONS
int main()
{
  // Define our functions.
  using func1 = Cb_t<>;

  NontrivialType nt;
  std::unique_ptr<func1> f1 = std::make_unique<func1>(
      [nt](void)
      {
        nt.printSomething();
      }
  );

  // Add to the map.
  std::type_index index1(typeid(f1));
  callbacks.insert(callbacks_t::value_type(index1, std::move(f1)));

  // Call the callbacks.
  call(index1);

  return 0;
}

This produces the following output (using G++ 5.1 with no optimization):

NontrivialType{void}
NontrivialType{const NontrivialType&}
NontrivialType{NontrivialType&&}
NontrivialType{NontrivialType&&}
NontrivialType{const NontrivialType&}
Calling the destructor for a NontrivialType!
Calling the destructor for a NontrivialType!
Calling the destructor for a NontrivialType!
Calling NontrivialType::printSomething()!
Calling the destructor for a NontrivialType!

I count five constructor calls and four destructor calls. I think that indicates that my analysis is correct--the container cannot properly destroy the instance it owns.

Is this approach salvageable? When I add a virtual =default destructor to Func_t, I see a matching number of ctor/dtor calls:

NontrivialType{void}
NontrivialType{const NontrivialType&}
NontrivialType{NontrivialType&&}
NontrivialType{NontrivialType&&}
NontrivialType{const NontrivialType&}
Calling the destructor for a NontrivialType!
Calling the destructor for a NontrivialType!
Calling the destructor for a NontrivialType!
Calling NontrivialType::printSomething()!
Calling the destructor for a NontrivialType!
Calling the destructor for a NontrivialType!

... so I think this change might be sufficient. Is it?

Aucun commentaire:

Enregistrer un commentaire