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?
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))
{}
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With