Click here to Skip to main content
15,881,757 members
Please Sign up or sign in to vote.
4.00/5 (1 vote)
See more:
Suppose I have the following toy class:

C++
class my_class
{
	// A, B and C types defined elsewhere...

	typedef std::shared_ptr<A> a_ptr_type;
	typedef std::shared_ptr< B> b_ptr_type;
	typedef std::shared_ptr<C> c_ptr_type;

	a_ptr_type a_ptr_;
	b_ptr_type b_ptr_;
	c_ptr_type c_ptr_;

public:

	my_class(const A& a, const B& b) :
	a_ptr_(std::make_shared<A>(a)),
	b_ptr_(std::make_shared< B>(b)),
	c_ptr_(nullptr)
	{}

	my_class(const A& a, const C& c) :
	a_ptr_(std::make_shared<A>(a)),
	b_ptr_(nullptr),
	c_ptr_(std::make_shared<C>(c))
	{}

// ...
};


The previous class has two constructor, in such a way that either b_ptr_ or c_ptr_ is always null.

If I'd want my_class to accept rvalues for A, B and C types as construction parameters, I could templatize the constructors the following way:

C++
class my_class
{
	// A, B and C types defined elsewhere...

	typedef std::shared_ptr<A> a_ptr_type;
	typedef std::shared_ptr< B> b_ptr_type;
	typedef std::shared_ptr<C> c_ptr_type;

	a_ptr_type a_ptr_;
	b_ptr_type b_ptr_;
	c_ptr_type c_ptr_;

public:

	template <typename Type>
	my_class(Type&& a, const B& b) :
	a_ptr_(std::make_shared<A>(std::forward<Type>(a))),
	b_ptr_(std::make_shared< B>(b)),
	c_ptr_(nullptr)
	{}

	template <typename Type>
	my_class(Type&& a, B&& b) :
	a_ptr_(std::make_shared<A>(std::forward<Type>(a))),
	b_ptr_(std::make_shared< B>(std::move(b))),
	c_ptr_(nullptr)
	{}

	template <typename Type>
	my_class(Type&& a, const C& c) :
	a_ptr_(std::make_shared<A>(std::forward<Type>(a))),
	b_ptr_(nullptr),
	c_ptr_(std::make_shared<C>(c))
	{}

	template <typename Type>
	my_class(Type&& a, C&& c) :
	a_ptr_(std::make_shared<A>(std::forward<Type>(a))),
	b_ptr_(nullptr),
	c_ptr_(std::make_shared<C>(std::move(c)))
	{}

// ...
};


The solution above works and my question is:

Are there other more compact solutions, keeping the same functionality with fewer constructors?

Thanks in advance.
Posted

1 solution

I think that with following you can go back to version with two constructors:

C++
class my_class
{
   // A, B and C types defined elsewhere...

   typedef std::shared_ptr< A > a_ptr_type;
   typedef std::shared_ptr< B> b_ptr_type;
   typedef std::shared_ptr< C > c_ptr_type;
 
   a_ptr_type a_ptr_;
   b_ptr_type b_ptr_;
   c_ptr_type c_ptr_;
 
   template < class T > T&& store_impl(T&& v, std::false_type const& )
   {
      return std::forward<T>( v );
   }
   template < class T > T&& store_impl(T&& v, std::true_type const& )
   {
      return std::move( v );
   }
   template < class Type >
   Type&& store(Type&& v)
   {
      return store_impl(std::forward<Type>(v),
         typename std::is_rvalue_reference<Type>::type());
   }
   
   template < class T >
   using remove_const_reference = 
   typename std::remove_const<typename std::remove_reference< T >::type>;

//if your compiler has a problem with template aliases use classical approach:
//template< class T >
//struct remove_const_reference {
//  typedef typename std::remove_const< typename std::remove_reference< T >::type >::type type;
//};

public:

   template < class Type, class U, typename std::enable_if< std::is_same< B,
      typename remove_const_reference< U >::type >::value, int >::type = 0 >
   my_class(Type&& a, U&& b) :
   	a_ptr_(std::make_shared< A >(std::forward< Type >(a))),
   	b_ptr_(std::make_shared< B >(store(std::forward< U >(b)))),
	c_ptr_(nullptr)
	{
	}
 
   template < class Type, class U, typename std::enable_if< std::is_same< C,
      typename remove_const_reference< U >::type >::value, int >::type = 0 >
   my_class(Type&& a, U&& c) :
   	a_ptr_(std::make_shared< A >(std::forward< Type >(a))),
	b_ptr_(nullptr),
   	c_ptr_(std::make_shared< C >(store(std::forward< U >(c))))
	{
	}

// ...
};


Since you have always two input types, and only assignment of the members variable is vary two ctors should be sufficient. To determine which member should be assigned function overloading and type traits are used (you can always use template class and its specialization instead of overloading store() function - eg.
C++
template< class T, bool is_rval > struct store;
template< class T> struct store< T, false>
{
   static T&& apply(T&& ) { ... }  // forwarding
};
template<class T> struct struct store< T, true >
{
   static T&& apply(T&& ) { ... }  // using std::move
};

).

Using SFINAE, reference collapsing rules and function overloading two constructors have been eliminated. Drawback is that we need to add store() layer which assures that proper constructors of A,B or C will be invoked (copy vs. move ctor). Hence, the code isn't much shorter.
Although, store() or similar can be moved outside the class and reused in different places. To reduce verbosity local macros can be used in some places.

Following code uses presented by me approach and it was tested using clang++3.2, g++ 4.7.2 and g++ 4.8.0. Def or undef DEF_4CTORS to switch between version with four or two ctors. Observable behaviour is the same with both versions.

C++
//#define DEF_4CTORS
#include <iostream>
#include <memory>
#include <type_traits>
#include <string>

 
struct A {
   A(){
      std::cout << "A()" << std::endl;
   }
   A(A const& other) : s(other.s)
   {
      std::cout << "A(A const& other)" << std::endl;
   }
 
   A(A&& other) : s(std::move(other.s))
   {
      std::cout << "A(A&& other)" << std::endl;
   }
   
   std::string s{"A"};
};
struct B {
   B() {
      std::cout << "B()" << std::endl;
   }
   B(B const& other) : s(other.s)
   {
      std::cout << "B(B const& other)" << std::endl;
   }
 
   B(B&& other) : s(std::move(other.s))
   {
      std::cout << "B(B&& other)" << std::endl;
   }
   
   std::string s{"B"};
};
struct C {
   C() {
      std::cout << "C()" << std::endl;
   }
   C(C const& other) : s(other.s)
   {
      std::cout << "C(C const& other)" << std::endl;
   }
 
   C(C&& other) : s(std::move(other.s))
   {
      std::cout << "C(C&& other)" << std::endl;
   }
 
   std::string s{"C"};
};
 
class my_class
{
   // A, B and C types defined elsewhere...

   typedef std::shared_ptr< A > a_ptr_type;
   typedef std::shared_ptr< B > b_ptr_type;
   typedef std::shared_ptr< C > c_ptr_type;
 
   a_ptr_type a_ptr_;
   b_ptr_type b_ptr_;
   c_ptr_type c_ptr_;
 
public:
#ifdef DEF_4CTORS
	template <typename Type>
	my_class(Type&& a, const B& b) :
	a_ptr_(std::make_shared<A>(std::forward<Type>(a))),
	b_ptr_(std::make_shared< B>(b)),
	c_ptr_(nullptr)
	{
      std::cout << "my_class(Type&& a, const B& b)" << std::endl;
	}
 
	template <typename Type>
	my_class(Type&& a, B&& b) :
	a_ptr_(std::make_shared<A>(std::forward<Type>(a))),
	b_ptr_(std::make_shared< B>(std::move(b))),
	c_ptr_(nullptr)
	{
      std::cout << "my_class(Type&& a, B&& b)" << std::endl;
	}
 
	template <typename Type>
	my_class(Type&& a, const C& c) :
	a_ptr_(std::make_shared<A>(std::forward<Type>(a))),
	b_ptr_(nullptr),
	c_ptr_(std::make_shared<C>(c))
	{
      std::cout << "my_class(Type&& a, const C& c)" << std::endl;
	}
 
	template <typename Type>
	my_class(Type&& a, C&& c) :
	a_ptr_(std::make_shared<A>(std::forward<Type>(a))),
	b_ptr_(nullptr),
	c_ptr_(std::make_shared<C>(std::move(c)))
	{
      std::cout << "my_class(Type&& a, C&& c)" << std::endl;
	}
 
#else
private: 
   template<class T> T&& store_impl(T&& v, std::false_type const& )
   {
      return std::forward<T>( v );
   }
   template<class T> T&& store_impl(T&& v, std::true_type const& )
   {
      return std::move( v );
   }
   template<class Type>
   Type&& store(Type&& v)
   {
      return store_impl(std::forward<Type>(v),
         typename std::is_rvalue_reference<Type>::type());
   }
 
   template < class T >
   using remove_const_reference = 
   typename std::remove_const<typename std::remove_reference< T >::type>;
 
//if your compiler has a problem with template aliases use classical approach:
//template< class T >
//struct remove_const_reference {
//  typedef typename std::remove_const< typename std::remove_reference< T >::type >::type type;
//};

public:
 
   template < class Type, class U, typename std::enable_if< std::is_same< B,
      typename remove_const_reference< U >::type >::value, int >::type = 0 >
   my_class(Type&& a, U&& b) :
      a_ptr_(std::make_shared< A >(std::forward< Type >(a))),
      b_ptr_(std::make_shared< B >(store(std::forward< U >(b)))),
      c_ptr_(nullptr)
      {
         std::cout << "my_class(Type&& a, U&& b)" << std::endl;
      }
 
   template < class Type, class U, typename std::enable_if< std::is_same< C,
      typename remove_const_reference< U >::type >::value, int >::type = 0 >
   my_class(Type&& a, U&& c) :
      a_ptr_(std::make_shared< A >(std::forward< Type >(a))),
      b_ptr_(nullptr),
      c_ptr_(std::make_shared< C >(store(std::forward< U >(c))))
      {
         std::cout << "my_class(Type&& a, U&& c)" << std::endl;
      }
   
#endif
// ...
};
 
int main()
{
 
   my_class c1( (A()), (B()) );
   my_class c2( (A()), (C()) );
   A a;
   const B b;
   C c;
   my_class c3(a, b);
   my_class c4(a, c);
   
}


I hope it can help you and using my insights you'll find suitable solution for you.
Regards!
 
Share this answer
 
v4
Comments
David Serrano Martínez 3-Mar-13 3:07am    
Absolutely brilliant piece of metaprogramming! I have learnt a lot with your reply. Great work!
The main drawback is that this solution is quite difficult. I have been going over your code for a long, long time till I finally have understood ;-)

After doing my own checks, I have verified both solutions (four constructors and two constructors) are not exactly equivalent. Try this slightly changed main:

int main()
{

my_class c1( (A()), (B()) );
my_class c2( (A()), (C()) );
A a;
const B b;
const C c;
my_class c3(a, b);
my_class c4(a, c);
}

Seemingly std::remove_reference does not remove also constness. Any suggestions?

Thank you for such an elaborated answer.
David Serrano Martínez 3-Mar-13 5:33am    
After some trials, it seems it works with const arguments by substituting:

typename std::remove_reference<U>::type>::value, int>::type=0

for

typename std::remove_const<typename std::remove_reference<U>::type>::type>::value, int>::type=0

Unfortunately I have not found any std::remove_const_reference<> to simplify things a little bit.

With this tiny change, I consider your solution accepted ;-).

Thanks again.
Lukasz Gwizdz (Member 2097797) 3-Mar-13 16:59pm    
Hello again!

Yeah, You have right. I forgot about removing topomost cv-qualifiers. But, as I see, you managed to fix it easily. And you have right too. There is no std::remove_const_reference. You need to compose type traits in order to accomplish such effect. In my opinion, it is correct that there isn't remove_const_reference. Such trait would be serving two purposes. The fine grain of highly related type traits supports high cohesion. The only type trait class I can think about which in fact is composed of two operations is std::remove_cv (and std::add_cv) - which, in general, removes cv-qulifiers (const or volatile). For your purposes you can compose such classes on your own eg.:
template< class T >
struct remove_const_reference {
typedef typename std::remove_const<typename std::remove_reference<t="">::type>::type type;
};

In this case you can also use template alias. So revised version could look like this:
class my_class
{
// A, B and C types defined elsewhere...

typedef std::shared_ptr a_ptr_type;
typedef std::shared_ptr< B> b_ptr_type;
typedef std::shared_ptr<c> c_ptr_type;

a_ptr_type a_ptr_;
b_ptr_type b_ptr_;
c_ptr_type c_ptr_;

template<class t=""> T&& store_impl(T&& v, std::false_type const& )
{
return std::forward<t>( v );
}
template<class t=""> T&& store_impl(T&& v, std::true_type const& )
{
return std::move( v );
}
template<class type="">
Type&& store(Type&& v)
{
return store_impl(std::forward<type>(v),
typename std::is_rvalue_reference<type>::type());
}

template<class t="">
using remove_const_reference = typename std::remove_const<typename std::remove_reference<="" t="">::type>;

// if your compiler has a problem with template aliases use classical approach:
// template< class T >
// struct remove_const_reference {
// typedef typename std::remove_const<typename std::remove_reference<t="">::type>::type type;
// };

public:

template < class Type, class U, typename std::enable_if< std::is_same< B,
typename remove_const_reference< U >::type >::value, int >::type = 0 >
my_class(Type&& a, U&& b) :
a_ptr_(std::make_shared< A >(std::forward< Type >(a))),
b_ptr_(std::make_shared< B >(store(std::forward< U >(b)))),
c_ptr_(nullptr)
{
}

template < class Type, class U, typename std::enable_if< std::is_same< C,
typename remove_const_reference< U >::type >::value, int >::type = 0 >
my_class(Type&& a, U&& c) :
a_ptr_(std::make_shared< A >(std::forward< Type >(a))),
b_ptr_(nullptr),
c_ptr_(std::make_shared< C >(store(std::forward< U >(c))))
{
}

// ...
};

Depending on your needs you can even create your own traits using partial template specialization (see implementation of type_traits or boost::type_traits library as well). Remember that you can always use std::true_type or std::false_type to conform standard behaviour/interface eg.:
// similar to std::is_same
template<class t,="" class="" u="">
struct is_same_type : std::false_type
{};
template<class t="">
struct is_same_type<t,t> : std::true_type { };

// another example, notice usage using declaration instead of typedef declaration
using False = std::false_type;
template<class t="">
struct is_pointer : False
{};

typedef std::true_type True;
template<class t="">
struct is_pointer<t*> : True
{};

Of course, if you don't want use std::integral_constant (std::true_type, std::false_type) you may always define your classes eg.:
struct True
{
static constexpr bool value = true;
};
struct False
{
static constexpr bool value = false;
};

Anyway, thank you for accepting my solution.
It was very kind and definitely made my day ;)

Regards!
Lukasz Gwizdz
David Serrano Martínez 11-Mar-13 15:32pm    
Hi Lukasz!

For your information. The solution also works without the use of store(...). For instance: instead of store(std::forward<...>(...)), it works by typing std::forward<...>(...), so the code simplifies even more.

Greetings.
Lukasz Gwizdz (Member 2097797) 11-Mar-13 18:12pm    
Yes, indeed. I also realized it later. Perfect forwarding does the job here. Because of std::forward, the entire tag dispatching with store() layer is useless. It does redundant job. I cannot comprehend now why I did it. Anyway, C++ rocks! ;)

Thanks for info. Should I update the answer?

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900