Disclaimer I am not a computer scientist, nor a C++ expert. If the terminology I use in this post is not 100% accurate, I apologize.
In the previous post, I showed how to implement a basic type erasure class that could be used with legacy inheritance-base classes. The pattern required three pieces, an abstract base class (which may already exist in your code base), a wrapper class to bring unrelated types into the inheritance tree, and a type erasing container that provides value semantics and manages the object life time.
The end result looked like this:
#include <memory>
#include <iostream>
#include <vector>
/**
* A class to define the methods that will callable at runtime.
*/
class Interface
{
public:
virtual int getOpt1() const = 0;
virtual void setOpt1(int) = 0;
virtual ~Interface() = default;
virtual std::unique_ptr<Interface> copy() const = 0;
};
/**
* A class for bringing new types into an inheritance tree non-intrusively.
*/
template<size_t N>
struct priority : priority<N-1>{};
template<>
struct priority<0>{};
template<typename T>
class Wrapper : public Interface, T
{
public:
using T::T;
using T::operator=;
Wrapper(const T& t) : T(t) {}
Wrapper(const T&& t) : T(t) {}
virtual int getOpt1() const final{ return _getOpt1(priority<1>{}); }
virtual void setOpt1(int a) final { _setOpt1(a,priority<1>{}); }
virtual std::unique_ptr<Interface> copy() const {return std::unique_ptr<Interface>(new Wrapper<T>(*static_cast<const T*>(this)));}
protected:
template<typename TT = T>
auto _getOpt1(priority<0>) const -> decltype(static_cast<const TT*>(this)->getOpt1()) { return static_cast<const TT*>(this)->getOpt1(); }
template<typename TT = T>
auto _getOpt1(priority<1>) const -> decltype(map_getOpt1(*static_cast<const TT*>(this))) { return map_getOpt1(*static_cast<const TT*>(this)); }
template<typename TT = T>
auto _setOpt1(int a,priority<0>) -> decltype(static_cast<TT*>(this)->setOpt1(a)) { static_cast<T*>(this)->setOpt1(a); }
template<typename TT = T>
auto _setOpt1(int a,priority<1>) -> decltype(map_setOpt1(*static_cast<TT*>(this), a)) {map_setOpt1(*static_cast<TT*>(this), a);}
};
/**
* A type-erasing container that can store and use any type providing the methods listed in Interface
*/
class Any
{
private:
std::unique_ptr<Interface> m_storage;
public:
int getOpt1() const { return m_storage->getOpt1(); }
void setOpt1(int a) const { return m_storage->setOpt1(a); }
// type erasure magic!
template<typename T>
Any(const Wrapper<T>& t) : m_storage(new Wrapper<T>(t)) { }
template<typename T>
Any(const Wrapper<T>&& t) : m_storage(new Wrapper<T>(t)) { }
template<typename T>
Any(const T& t) : m_storage(new Wrapper<T>(t)) { }
template<typename T>
Any(const T&& t) : m_storage(new Wrapper<T>(t)) { }
Any(const Any& a): m_storage( a.m_storage->copy() ) { }
Any& operator=(const Any& a)
{
m_storage = a.m_storage->copy();
return *this;
}
};
class A
{
public:
void setOpt1(int a){}
int getOpt1() const {return 1;}
};
class B
{
public:
void setOpt1(int a){}
int getOpt1() const {return 2;}
};
int main()
{
std::vector<Any> objects;
objects.push_back( A() );
objects.push_back( B() );
Any any = objects[0];
for(auto &item : objects )
std::cout << item.getOpt1() << "\n";
std::cout << any.getOpt1() << "\n";
}
Which gave the following output
1
2
1
In this post I want to modify this example to follow Sean Parent's guidelines in Better Code: Runtime Polymorphism. As usual, this post is mostly for me to use as a reference in the future, but hopefully you will also find it useful.
The guidelines I'm talking about are about the copy and assignment which will only involve the constructors and assignment operators, so I'm going to remove the SFINAE magic from the example.
First, I'll replace some direct calls to new (a no-no), with std::make_unique<...>(...), and then add some print statements to see what is getting called.
#include <memory>
#include <iostream>
#include <vector>
class Interface
{
public:
virtual int getOpt1() const = 0;
virtual void setOpt1(int) = 0;
virtual ~Interface() = default;
virtual std::unique_ptr<Interface> copy() const = 0;
};
/**
* A wrapper class to bring unrelated types into the inheritance tree.
*/
template<typename T>
class Wrapper : public Interface, T
{
public:
using T::T;
using T::operator=;
Wrapper(const T& t) : T(t) {}
Wrapper(const T&& t) : T(t) {}
virtual int getOpt1() const final{ return static_cast<const T*>(this)->getOpt1(); }
virtual void setOpt1(int a) final { static_cast<T*>(this)->setOpt1(a); }
virtual std::unique_ptr<Interface> copy() const {return std::make_unique<Wrapper<T>>(*static_cast<const T*>(this));}
};
/**
* A type-erasing container that can store and use any type providing the methods listed in Interface
*/
class Any
{
private:
std::unique_ptr<Interface> m_storage;
public:
int getOpt1() const { return m_storage->getOpt1(); }
void setOpt1(int a) const { return m_storage->setOpt1(a); }
// type erasure magic!
template<typename T>
Any(const Wrapper<T>& t) : m_storage(std::make_unique<Wrapper<T>>(t)) { std::cout << "ctor 1" << std::endl; }
template<typename T>
Any(const Wrapper<T>&& t) : m_storage(std::make_unique<Wrapper<T>>(t)) { std::cout << "ctor 2" << std::endl; }
// allow construction from the stored type
template<typename T>
Any(const T& t) : m_storage(std::make_unique<Wrapper<T>>(t)) { std::cout << "ctor 3" << std::endl; }
template<typename T>
Any(const T&& t) : m_storage(std::make_unique<Wrapper<T>>(t)) { std::cout << "ctor 4" << std::endl; }
// copy constructor
Any(const Any& a): m_storage( a.m_storage->copy() ) { std::cout << "copy" << std::endl;}
//
Any& operator=(const Any& a)
{
std::cout << "assign" << std::endl;
m_storage = a.m_storage->copy();
return *this;
}
};
class A
{
public:
A(){std::cout << "A ctor" << std::endl;}
void setOpt1(int a){}
int getOpt1() const {return 1;}
};
class B
{
public:
B(){std::cout << "B ctor" << std::endl;}
void setOpt1(int a){}
int getOpt1() const {return 2;}
};
int main()
{
std::cout << ">> Add to vector" << std::endl;
std::vector<Any> objects;
objects.push_back( A() );
objects.push_back( B() );
std::cout << ">> New assignment" << std::endl;
Any obja = objects[0];
std::cout << ">> Existing assignment" << std::endl;
obja = objects[1];
std::cout << ">> Existing assignment with temporary" << std::endl;
obja = Any(A());
}
Output:
>> Add to vector
A ctor
ctor 4
copy
ctor 4
B ctor
ctor 4
copy
ctor 4
copy
copy
>> New assignment
copy
copy
>> Existing assignment
assign
copy
>> Existing assignment with temporary
A ctor
ctor 4
assign
The guidelines we want to implement are:
- Our
Anyconstructors should take arguments by value, since they are sink arguments, and then move them into place. - The copy constructor has to call the virtualized copy (which we are already doing).
- Our move constructor can be defaulted.
- We have to explicitly provide a move assignment operator, but it can also be defaulted.
- The copy assignment operator can use the move assignment operator.
Making these changes, we have:
#include <memory>
#include <iostream>
#include <vector>
class Interface
{
public:
virtual int getOpt1() const = 0;
virtual void setOpt1(int) = 0;
virtual ~Interface() = default;
virtual std::unique_ptr<Interface> copy() const = 0;
};
/**
* A wrapper class to bring unrelated types into the inheritance tree.
*/
template<typename T>
class Wrapper : public Interface, T
{
public:
using T::T;
using T::operator=;
Wrapper(T t) : T(std::move(t)) {} // 1. pass by value
virtual int getOpt1() const final{ return static_cast<const T*>(this)->getOpt1(); }
virtual void setOpt1(int a) final { static_cast<T*>(this)->setOpt1(a); }
virtual std::unique_ptr<Interface> copy() const {return std::make_unique<Wrapper<T>>(*static_cast<const T*>(this));}
};
/**
* A type-erasing container that can store and use any type providing the methods listed in Interface
*/
class Any
{
private:
std::unique_ptr<Interface> m_storage;
public:
int getOpt1() const { return m_storage->getOpt1(); }
void setOpt1(int a) const { return m_storage->setOpt1(a); }
// type erasure magic!
template<typename T>
Any(Wrapper<T> t) : m_storage(std::make_unique<Wrapper<T>>(std::move(t))) { std::cout << "ctor 1" << std::endl; } // 1. pass by value
// allow construction from the stored type
template<typename T>
Any(T t) : m_storage(std::make_unique<Wrapper<T>>(std::move(t))) { std::cout << "ctor 3" << std::endl; } // 1. pass by value
// copy constructor
Any(const Any& a): m_storage( a.m_storage->copy() ) { std::cout << "copy" << std::endl;} // 2. virualized copy
// move constructor
Any(Any&& a) noexcept = default; // 3. defaulted move
// move assignment
Any& operator=(Any&& a) noexcept = default; // 4. defaulted move assignemnt
// copy assinment
Any& operator=(const Any& a) // 5. Create temporary with copy constructor and them move with move assignment
{
return *this = Any(a);
}
};
class A
{
public:
A(){std::cout << "A ctor" << std::endl;}
void setOpt1(int a){}
int getOpt1() const {return 1;}
};
class B
{
public:
B(){std::cout << "B ctor" << std::endl;}
void setOpt1(int a){}
int getOpt1() const {return 2;}
};
int main()
{
std::cout << ">> Add to vector" << std::endl;
std::vector<Any> objects;
objects.push_back( A() );
objects.push_back( B() );
std::cout << ">> New assignment" << std::endl;
Any obja = objects[0];
std::cout << ">> Existing assignment" << std::endl;
obja = objects[1];
std::cout << ">> Existing assignment with temporary" << std::endl;
obja = Any(A());
}
With these changes we get the following ouptut.
>> Add to vector
A ctor
ctor 3
B ctor
ctor 3
>> New assignment
copy
>> Existing assignment
copy
>> Existing assignment with temporary
A ctor
ctor 3
which has reduced the number of copies from seven to two. So, that's good.