Search code examples
templatesconstructorc++17template-argument-deductiontype-promotion

How to apply arithmetic type promotion within a constructor


Hypothetically, If I were creating a point class and I wanted it to deduced the type based on the arguments I would want it to promote the point class to the highest argument. For example:

template <class dtype>
class Point;

...

auto x = Point(1, 1.0); // Point<double> specialized
auto y = Point(1.0, 1); // Point<double> specialized

I am not sure how to achieve this within a constructor. I've been able to make it deduce the type from a function which calls an explicitly specialized constructor, but not from the constructor itself.

Here is my attempt so far:

#include <type_traits>

template <typename... Ts>
struct promoted_type_wrap;

template <typename T>
struct promoted_type_wrap<T> {
  using type = T;
};

template <typename T, typename U, typename... Ts>
struct promoted_type_wrap<T, U, Ts...> {
  using type = typename promoted_type_wrap<typename std::conditional<
    (sizeof(U) <= sizeof(T)), T, U >::type, Ts... >::type;
};

template <typename... Ts>
using promoted_type = typename promoted_type_wrap<Ts...>::type;

template <typename T>
using same_type = typename promoted_type_wrap<T>::type;

template <class dtype>
class Point {
protected:
  dtype x, y;

public:
  constexpr Point(const dtype x, const same_type<dtype> y)
    : x(x), y(y) {
  }
};

template <class dtype, class etype>
constexpr auto make_Point(const dtype x, const etype y) {
  return Point<promoted_type<dtype, etype>>(x, y);
}

void test() {
  constexpr auto x = make_Point(1, 2.0); // Point<double> specialized
  constexpr auto y = make_Point(1.0, 2); // Point<double> specialized
  constexpr auto z = Point(1, 2.0); // Point<int> specialized
  constexpr auto w = Point(1.0, 2); // Point<double> specialized
}

It makes sense why Point(1, 2.0) is specialized as a Point<int> since the first argument is an int which forces the second argument in the constructor as an int; however, I am unsure how to go about rewriting the constructor to behave like the pseudo-constructor factory.


Solution

  • however, I am unsure how to go about rewriting the constructor to behave like the pseudo-constructor factory.

    Not the constructor: you have to write a custom deduction guide.

    Something as follows

    template <typename T1, typename T2>
    Point(T1, T2) -> Point<promoted_type<T1, T2>>;
    

    The following is a full compiling example

    #include <type_traits>
    
    template <typename... Ts>
    struct promoted_type_wrap;
    
    template <typename T>
    struct promoted_type_wrap<T>
     { using type = T; };
    
    template <typename T, typename U, typename... Ts>
    struct promoted_type_wrap<T, U, Ts...>
     { using type = typename promoted_type_wrap<std::conditional_t<
          (sizeof(U) <= sizeof(T)), T, U >, Ts... >::type; };
    
    template <typename... Ts>
    using promoted_type = typename promoted_type_wrap<Ts...>::type;
    
    template <typename dtype>
    class Point
     {
       protected:
          dtype x, y;
    
       public:
          template <typename T1, typename T2>
          constexpr Point (T1 const & a, T2 const & b) : x(a), y(b)
           { }
     };
    
    template <typename T1, typename T2>
    Point(T1, T2) -> Point<promoted_type<T1, T2>>;
    
    int main ()
     {
       constexpr auto z = Point(1, 2.0); // now Point<double>
       constexpr auto w = Point(1.0, 2); // again Point<double> 
    
       static_assert( std::is_same_v<decltype(z), Point<double> const> );
       static_assert( std::is_same_v<decltype(w), Point<double> const> );
     }
    

    Off Topic: I don't think it's a good idea select the "promoted type" according the size of the type as in your

    template <typename T, typename U, typename... Ts>
    struct promoted_type_wrap<T, U, Ts...>
     { using type = typename promoted_type_wrap<std::conditional_t<
          (sizeof(U) <= sizeof(T)), T, U >, Ts... >::type; };
    

    Even ignoring other problems you have that, when you have different types of the same size, the selected type is the first one.

    By example, in my platform both g++ and clang++ have sizeof(long) == sizeof(float), so we get that

    constexpr auto z = Point(1l, 2.0); // <-- deduced as Point<long>
    constexpr auto w = Point(1.0, 2l); // <-- deduced as Point<double>
    
    static_assert( std::is_same_v<decltype(z), Point<long> const> );
    static_assert( std::is_same_v<decltype(w), Point<double> const> );
    

    I suggest the use of something that select the "preferred type" independently from the order of the types.

    It seems to me that you should use std::common_type as follows

    #include <type_traits>
    
    template <typename dtype>
    class Point
     {
       protected:
          dtype x, y;
    
       public:
          template <typename T1, typename T2>
          constexpr Point (T1 const & a, T2 const & b) : x(a), y(b)
           { }
     };
    
    template <typename T1, typename T2>
    Point(T1, T2) -> Point<std::common_type_t<T1, T2>>;
    
    int main ()
     {
       constexpr auto z = Point(1l, 2.0); // <-- deduced as Point<double>
       constexpr auto w = Point(1.0, 2l); // <-- deduced as Point<double>
    
       static_assert( std::is_same_v<decltype(z), Point<double> const> );
       static_assert( std::is_same_v<decltype(w), Point<double> const> );
     }