Reputation: 95
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
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
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