Eugen Wintersberger
Eugen Wintersberger

Reputation: 95

C++ type erasure with template copy and move constructor

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?

Upvotes: 5

Views: 1716

Answers (2)

Grizzly
Grizzly

Reputation: 20201

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))
   {}

Upvotes: 4

Dietmar K&#252;hl
Dietmar K&#252;hl

Reputation: 153955

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.

Upvotes: 1

Related Questions