tutorial - Idiomatic C++11 type promotion




modern c++ features (2)

There is a great paper on C++ for scientific computing where the author (T. Veldhuizen) suggests a traits-based approach to address type promotion. I have used such approach, and found it effective:

#include<iostream>
#include<complex>
#include<typeinfo>

template<typename T1, typename T2>
struct promote_trait{};

#define DECLARE_PROMOTION(A, B, C) template<> struct promote_trait<A, B> { using T_promote = C;};

DECLARE_PROMOTION(int, char, int);
DECLARE_PROMOTION(int, float, float);
DECLARE_PROMOTION(float, std::complex<float>, std::complex<float>);

// similarly for all possible type combinations...

template<typename T1, typename T2>
void product(T1 a, T2 b) {
  using T = typename promote_trait<T1, T2>::T_promote;
  T ans = T(a) * T(b);  
  std::cout<<"received "
           <<typeid(T1).name()<<"("<<a<<")"<<" * "
           <<typeid(T2).name()<<"("<<b<<")"<<" ==> "
           <<"returning "
           <<typeid(T).name()<<"("<<ans<<")"<<std::endl;
}

int main() {
  product(1, 'a');
  product(1, 2.0f);
  product(1.0f, std::complex<float>(1.0f, 2.0f));
  return 0;
}

Output:

received i(1) * c(a) ==> returning i(97)
received i(1) * f(2) ==> returning f(2)
received f(1) * St7complexIfE((1,2)) ==> returning St7complexIfE((1,2))

the name of the types returned by typeinfo is implementation dependent; your output may differ from mine, which used GCC 4.7.2 on OS X 10.7.4

In essence, the approach defines a promote_trait which contains simply one type definition: the type to which two types should be promoted when operating in a given manner. One needs to declare all possible promotions.

When one function receives both types, it relies on the promote_trait to deduce the correct, promoted type of the result. If a trait has not been defined for a given pair the code fails to compile (a desirable feature).

Now, the paper in question was written in 2000, and we know that C++ has evolved dramatically over the past decade. My question, then, is the following:

Is there a modern, idiomatic C++ 11 approach to dealing with type promotion as effective as the traits-based approach introduced by Veldhuizen?

Edit (on using std::common_type)

Based on Luc Danton's suggestion, I created the following code which uses std::common_type:

#include<iostream>
#include<complex>
#include<typeinfo>
#include<typeindex>
#include<string>
#include<utility>
#include<map>

// a map to homogenize the type names across platforms
std::map<std::type_index, std::string> type_names = {
  {typeid(char)                 , "char"},  
  {typeid(int)                  , "int"},
  {typeid(float)                , "float"},
  {typeid(double)               , "double"},
  {typeid(std::complex<int>)    , "complex<int>"},
  {typeid(std::complex<float>)  , "complex<float>"},
  {typeid(std::complex<double>) , "complex<double>"},
};

template<typename T1, typename T2>
void promotion(T1 a, T2 b) {
  std::string T1name = type_names[typeid(T1)];
  std::string T2name = type_names[typeid(T2)];
  std::string TPname = type_names[typeid(typename std::common_type<T1, T2>::type)];  
  std::cout<<T1name<<"("<<a<<") and "<<T2name<<"("<<b<<") promoted to "<<TPname<<std::endl;
}

int main() {
  promotion(1, 'a');
  promotion(1, 1.0);
  promotion(1.0, 1);
  promotion(std::complex<double>(1), 1);
  promotion(1.0f, 1);
  promotion(1.0f, 1.0);
  promotion(std::complex<int>(1), std::complex<double>(1));
  promotion(std::complex<double>(1), std::complex<int>(1));
  promotion(std::complex<float>(0, 2.0f), std::complex<int>(1));

  return 0;
}

with output:

int(1) and char(a) promoted to int
int(1) and double(1) promoted to double
double(1) and int(1) promoted to double
complex<double>((1,0)) and int(1) promoted to complex<double>
float(1) and int(1) promoted to float
float(1) and double(1) promoted to double
complex<int>((1,0)) and complex<double>((1,0)) promoted to complex<int>
complex<double>((1,0)) and complex<int>((1,0)) promoted to complex<int>
complex<float>((0,2)) and complex<int>((1,0)) promoted to complex<int>

I am surprised to notice that all but the last three promotions are what I expected. Why would complex<int> and complex<double> or complex<float> be promoted to complex<int>!?


As in catscradle answer, decltype and common_type (and custom specializations for it), are probably good C++11 replacement for the need of conversion traits that Veldhuizen have in mind. However, it will still fall short if you need to be still very specific for the evaluation of a function that maps two types into one (binary operator). (In other words decltype doesn't know about the mathematical domain of your problem).

My take is that you can resort to Boost.MPL maps http://www.boost.org/doc/libs/1_53_0/libs/mpl/doc/refmanual/map.html, this doesn't even require C++11, it is just that MPL wasn't written at that time:

#include<iostream>
#include<complex>
#include<typeinfo>
#include <boost/mpl/map.hpp>
#include <boost/mpl/at.hpp>
// all traits in one place, no need for MACROS or C++11, compile error if the case does not exist.
using namespace boost::mpl;
typedef map<
    pair<pair<int, char>, int>,
    pair<pair<int, float>, int>,
    pair<pair<float, std::complex<float> >, std::complex<float> >
> mapped_promotion;

template<typename T1, typename T2>
void product(T1 a, T2 b) {
  typedef typename at<mapped_promotion, pair<T1, T2> >::type T;

  T ans = T(a) * T(b);  
  std::cout<<"received "
           <<typeid(T1).name()<<"("<<a<<")"<<" * "
           <<typeid(T2).name()<<"("<<b<<")"<<" ==> "
           <<"returning "
           <<typeid(T).name()<<"("<<ans<<")"<<std::endl;
}

int main() {
  product(1, 'a');
  product(1, 2.0f);
  product(1.0f, std::complex<float>(1.0f, 2.0f));
  return 0;
}

Another extra benefit of using MPL is that you can then easily move to Boost.Fusion later, which usually is the case once you start dealing with "algebras" of types. And there is nothing to replace the functionality of Boost.Fusion in C++11 core language.


What follows is a more general solution, you can stop reading if the above was enough for your application, that combines MPL and decltype and requires C++11 which allows for unspecified pair of types to default to the decltype solution if it is any good, the trick is to see if the return of the mpl::map is the metatype void_ (pair not found).

...
#include <type_traits> 
//specific promotions
using namespace boost::mpl;
typedef map<
    pair<pair<int, char>, int>,
    pair<pair<int, float>, int>,
    pair<pair<float, std::complex<float> >, std::complex<float> >
> specific_mapped_promotion;

//promotion for unspecified combinations defaults to decltype type deduction.
template<class P1, class P2>
struct loose_mapped_promotion : std::conditional<
    std::is_same<typename at<specific_mapped_promotion, pair<P1, P2> >::type, mpl_::void_>::value,
    decltype( std::declval<P1>()*std::declval<P2>() ),
    typename at<specific_mapped_promotion, pair<P1, P2> >::type
> {};
template<typename T1, typename T2>
void product(T1 a, T2 b) {
  typedef typename loose_mapped_promotion<T1, T2>::type T;

  T ans = T(a) * T(b);
  ...
}
int main() {
   product(1.0, std::complex<double>(1.0f, 2.0f)); // now accepted, although no explicit trait was made
}

On a final note: it is apparently ok to overload std::common_type for special cases, if you want to use it: http://www.cplusplus.com/reference/type_traits/common_type/


I think you can use decltype for this:

template <typename T, typename U>
void product(T t, U u) 
{
    std::cout << typeid(decltype(t * u)).name() << std::endl;
}

Or with declval:

#include <utility>
template <typename T, typename U>
void product() 
{
    std::cout << typeid(decltype(std::declval<T>() * std::declval<U>())).name() << std::endl;
}

EDIT For T ans = T(a) * T(b); you can just use auto, auto ans = T(a) * T(b);





type-promotion