Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ type erasure with template copy and move constructor

Tags:

c++

I recently started learning about type erasures. It turned out that this technique can greatly simplify my life. Thus I tried to implement this pattern. However, I experience some problems with the copy- and move-constructor of the type erasure class. Now, lets first have a look on the code, which is quite straight forward

#include<iostream>
class A //first class
{
    private:
        double _value;
    public:
        //default constructor
        A():_value(0) {}
        //constructor
        A(double v):_value(v) {}
        //copy constructor
        A(const A &o):_value(o._value) {}
        //move constructor
        A(A &&o):_value(o._value) { o._value = 0; }

        double value() const { return _value; }
};

class B //second class
{
    private:
        int _value;
    public:
        //default constructor
        B():_value(0) {}
        //constructor
        B(int v):_value(v) {}
        //copy constructor
        B(const B &o):_value(o._value) {}
        //move constructor
        B(B &&o):_value(o._value) { o._value = 0; }

        //some public member
        int value() const { return _value; }
};

class Erasure //the type erasure
{
    private:
        class Interface  //interface of the holder
        {
            public:
                virtual double value() const = 0;
        };

        //holder template - implementing the interface
        template<typename T> class Holder:public Interface
        {
            public:
                T _object;
            public:
                //construct by copying o
                Holder(const T &o):_object(o) {}
                //construct by moving o
                Holder(T &&o):_object(std::move(o)) {}
                //copy constructor
                Holder(const Holder<T> &o):_object(o._object) {}
                //move constructor
                Holder(Holder<T> &&o):_object(std::move(o._object)) {}

                //implements the virtual member function
                virtual double value() const
                {
                    return double(_object.value());
                }
        };

        Interface *_ptr; //pointer to holder
    public:
        //construction by copying o
        template<typename T> Erasure(const T &o):
             _ptr(new Holder<T>(o))
        {}

        //construction by moving o
        template<typename T> Erasure(T &&o):
            _ptr(new Holder<T>(std::move(o)))
        {}

        //delegate
        double value() const { return _ptr->value(); }
};

int main(int argc,char **argv)
{
    A a(100.2344);
    B b(-100);

    Erasure g1(std::move(a));
    Erasure g2(b);

    return 0;
}

As a compiler I use gcc 4.7 on a Debian testing system. Assuming the code is stored in a file named terasure.cpp the build leads to the following error message

$> g++ -std=c++0x -o terasure terasure.cpp
terasure.cpp: In instantiation of ‘class Erasure::Holder<B&>’:
terasure.cpp:78:45:   required from ‘Erasure::Erasure(T&&) [with T = B&]’
terasure.cpp:92:17:   required from here
terasure.cpp:56:17: error: ‘Erasure::Holder<T>::Holder(T&&) [with T = B&]’ cannot be   overloaded
terasure.cpp:54:17: error: with ‘Erasure::Holder<T>::Holder(const T&) [with T = B&]’
terasure.cpp: In instantiation of ‘Erasure::Erasure(T&&) [with T = B&]’:
terasure.cpp:92:17:   required from here
terasure.cpp:78:45: error: no matching function for call to   ‘Erasure::Holder<B&>::Holder(std::remove_reference<B&>::type)’
terasure.cpp:78:45: note: candidates are:
terasure.cpp:60:17: note: Erasure::Holder<T>::Holder(Erasure::Holder<T>&&) [with T = B&]
terasure.cpp:60:17: note:   no known conversion for argument 1 from ‘std::remove_reference<B&>::type {aka B}’ to ‘Erasure::Holder<B&>&&’
terasure.cpp:58:17: note: Erasure::Holder<T>::Holder(const Erasure::Holder<T>&) [with T = B&]
terasure.cpp:58:17: note:   no known conversion for argument 1 from ‘std::remove_reference<B&>::type {aka B}’ to ‘const Erasure::Holder<B&>&’
terasure.cpp:54:17: note: Erasure::Holder<T>::Holder(const T&) [with T = B&]
terasure.cpp:54:17: note:   no known conversion for argument 1 from ‘std::remove_reference<B&>::type {aka B}’ to ‘B&’

It seems that for Erasure g2(b); the compiler still tries to use the move constructor. Is this intended behavior of the compiler? Do I missunderstand something in general with the type erasure pattern? Does someone have an idea how to get this right?

like image 838
Eugen Wintersberger Avatar asked Oct 05 '22 17:10

Eugen Wintersberger


2 Answers

As evident from the compiler errors, the compiler is trying to instantiate your Holder class for T = B&. This means that the class would store a member of a reference type, which gives you some problems on copy and such.

The problem lies in the fact that T&& (for deduced template arguments) is an universal reference, meaning it will bind to everything. For r-values of B it will deduce T to be B and bind as an r-value reference, for l-values it will deduce T to be B& and use reference collapsing to interpret B& && as B& (for const B l-values it would deduce T to be const B& and do the collapsing). In your example b is a modifiable l-value, making the constructor taking T&& (deduced to be B&) a better match then the const T& (deduced to be const B&) one. This also means that the Erasure constructor taking const T& isn't really necessary (unlike the one for Holder due to T not being deduced for that constructor).

The solution to this is to strip the reference (and probably constness, unless you want a const member) from the type when creating your holder class. You should also use std::forward<T> instead of std::move, since as mentioned the constructor also binds to l-values and moving from those is probably a bad idea.

    template<typename T> Erasure(T&& o):
        _ptr(new Holder<typename std::remove_cv<typename std::remove_reference<T>::type>::type>(std::forward<T>(o))
    {}

There is another bug in your Erasure class, which won't be caught by the compiler: You store your Holder in a raw pointer to heap allocated memory, but have neither custom destructor to delete it nor custom handling for copying/moving/assignment (Rule of Three/Five). One option to solve that would be to implement those operations (or forbid the nonessential ones using =delete). However this is somewhat tedious, so my personal suggestion would be not to manage memory manually, but to use a std::unique_ptr for memory management (won't give you copying ability, but if you want that you first need to expand you Holder class for cloning anyways).

Other points to consider: Why are you implementing custom copy/move constructors for Erasure::Holder<T>, A and B? The default ones should be perfectly fine and won't disable the generation of a move assignment operator.

Another point is that Erasure(T &&o) is problematic in that it will compete with the copy/move constructor (T&& can bind to Èrasure& which is a better match then both const Erasure& and Erasure&&). To avoid this you can use enable_if to check against types of Erasure, giving you something similar to this:

    template<typename T, typename Dummy = typename std::enable_if<!std::is_same<Erasure, std::remove_reference<T>>::value>::type>
    Erasure(T&& o):
        _ptr(new Holder<typename std::remove_cv<typename std::remove_reference<T>::type>::type>(std::forward<T>(o))
   {}
like image 86
Grizzly Avatar answered Oct 08 '22 20:10

Grizzly


Your problem is that the type T is deduced to be a reference by your constructor taking a universal reference. You want to use something along the lines of this:

#include <type_traits>

class Erasure {
    ....

    //construction by moving o
    template<typename T>
    Erasure(T &&o):
        _ptr(new Holder<typename std::remove_reference<T>::type>(std::forward<T>(o)))
    {
    }
};

That is, you need to remove any references deduced from T (and probably also any cv qualifier but the correction doesn't do that). and then you don't want to std::move() the argument o but std::forward<T>() it: using std::move(o) could have catastrophic consequences in case you actually do pass a non-const reference to a constructor of Erasure.

I didn't pay too much attention to the other code put as far as I can tell there also a few semantic errors (e.g., you either need some form of reference counting or a form of clone()int the contained pointers, as well as resource control (i.e., copy constructor, copy assignment, and destructor) in Erasure.

like image 43
Dietmar Kühl Avatar answered Oct 08 '22 21:10

Dietmar Kühl