Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Uses of a C++ Arithmetic Promotion Header

I've been playing around with a set of templates for determining the correct promotion type given two primitive types in C++. The idea is that if you define a custom numeric template, you could use these to determine the return type of, say, the operator+ function based on the class passed to the templates. For example:

// Custom numeric class
template <class T>
struct Complex {
    Complex(T real, T imag) : r(real), i(imag) {}
    T r, i;
// Other implementation stuff
};

// Generic arithmetic promotion template
template <class T, class U>
struct ArithmeticPromotion {
    typedef typename X type;  // I realize this is incorrect, but the point is it would
                              // figure out what X would be via trait testing, etc
};

// Specialization of arithmetic promotion template
template <>
class ArithmeticPromotion<long long, unsigned long> {
    typedef typename unsigned long long type;
}

// Arithmetic promotion template actually being used
template <class T, class U>
Complex<typename ArithmeticPromotion<T, U>::type>
operator+ (Complex<T>& lhs, Complex<U>& rhs) {
    return Complex<typename ArithmeticPromotion<T, U>::type>(lhs.r + rhs.r, lhs.i + rhs.i);
}

If you use these promotion templates, you can more or less treat your user defined types as if they're primitives with the same promotion rules being applied to them. So, I guess the question I have is would this be something that could be useful? And if so, what sorts of common tasks would you want templated out for ease of use? I'm working on the assumption that just having the promotion templates alone would be insufficient for practical adoption.

Incidentally, Boost has something similar in its math/tools/promotion header, but it's really more for getting values ready to be passed to the standard C math functions (that expect either 2 ints or 2 doubles) and bypasses all of the integral types. Is something that simple preferable to having complete control over how your objects are being converted?

TL;DR: What sorts of helper templates would you expect to find in an arithmetic promotion header beyond the machinery that does the promotion itself?

like image 272
BenTrofatter Avatar asked Mar 11 '10 15:03

BenTrofatter


People also ask

What is type promotion in C?

Type promotion in C is a method to convert any variable from one datatype to another. C allows variables of different datatypes to be present in a single expression. There are different types of type conversions available in C. They are Implicit type conversion and Explicit type conversion.

What is integer promotion in C?

Some data types like char , short int take less number of bytes than int, these data types are automatically promoted to int or unsigned int when an operation is performed on them. This is called integer promotion. For example no arithmetic calculation happens on smaller types like char, short and enum.


2 Answers

For this, what you can use is the ?: operator. It will give you the common type between two types. First, if the two types are the same, you are fine. Then, if the types differ, you invoke the ?: and see what type you get back.

You need to special case the non-promoted types char, short and their unsigned/signed versions thereof since applied to two of such operands of differing types, the result will be neither of them. You need also take care of the case where two classes can be converted to promoted arithmetic types. To get these right, we check whether the result of ?: is a promoted arithmetic type (in the spirit of clause 13.6), and use that type then.

// typedef eiher to A or B, depending on what integer is passed
template<int, typename A, typename B>
struct cond;

#define CCASE(N, typed) \
  template<typename A, typename B> \
  struct cond<N, A, B> { \
    typedef typed type; \
  }

CCASE(1, A); CCASE(2, B);
CCASE(3, int); CCASE(4, unsigned int);
CCASE(5, long); CCASE(6, unsigned long);
CCASE(7, float); CCASE(8, double);
CCASE(9, long double);

#undef CCASE

// for a better syntax...
template<typename T> struct identity { typedef T type; };

// different type => figure out common type
template<typename A, typename B>
struct promote {
private:
  static A a;
  static B b;

  // in case A or B is a promoted arithmetic type, the template
  // will make it less preferred than the nontemplates below
  template<typename T>
  static identity<char[1]>::type &check(A, T);
  template<typename T>
  static identity<char[2]>::type &check(B, T);

  // "promoted arithmetic types"
  static identity<char[3]>::type &check(int, int);
  static identity<char[4]>::type &check(unsigned int, int);
  static identity<char[5]>::type &check(long, int);
  static identity<char[6]>::type &check(unsigned long, int);
  static identity<char[7]>::type &check(float, int);
  static identity<char[8]>::type &check(double, int);
  static identity<char[9]>::type &check(long double, int);

public:
  typedef typename cond<sizeof check(0 ? a : b, 0), A, B>::type
    type;
};

// same type => finished
template<typename A>
struct promote<A, A> {
  typedef A type;
};

If your Complex<T> types can be converted into each other, ?: won't find a common type. You could specialize promote to tell it how to figure out a common type of two Complex<T>:

template<typename T, typename U>
struct promote<Complex<T>, Complex<U>> {
  typedef Complex<typename promote<T, U>::type> type;
};

Usage is simple:

int main() {
  promote<char, short>::type a;
  int *p0 = &a;

  promote<float, double>::type b;
  double *p1 = &b;

  promote<char*, string>::type c;
  string *p2 = &c;
}

Note that for real-world uses, you should best catch a few cases I left out for simplicity, for example <const int, int> should be handled similar to <T, T> (you best first strip const and volatile and convert T[N] to T* and T& to T and afterwards delegate to the actual promote template - i.e do boost::remove_cv<boost::decay<T>>::type for both A and B before delegating them). If you don't do this, the call to check will end up in an ambiguity for these cases.

like image 67
Johannes Schaub - litb Avatar answered Sep 18 '22 15:09

Johannes Schaub - litb


This is definitely useful -- we use these sorts of things in the math library that I work on for correctly typing intermediate values in expressions. For example, you might have a templated addition operator:

template<typename Atype, typename Btype>
type_promote<Atype, Btype>::type operator+(Atype A, Btype B);

This way, you can write a generic operator that will handle different argument types, and it will return a value of the appropriate type to avoid precision loss in the expression that it appears in. It's also useful (in things like vector sums) for properly declaring internal variables within these operators.

As for the question of what ought to go with these: I just checked in our source code where we define them, and all we have there are just the simple ArithmeticPromotion declaration you describe -- three generic versions to resolve the complex-complex, complex-real, and real-complex variants using the specific real-real ones, and then a list of real-real ones -- about 50 lines of code in all. We don't have any other helper templates with them, and it doesn't (from our usage) look like there are any natural ones that we'd use.

(FWIW, if you don't want to write this yourself, download our source from http://www.codesourcery.com/vsiplplusplus/2.2/download.html, and pull out src/vsip/core/promote.hpp. That's even in the part of our library that's BSD-licensed, though it doesn't actually say so in the file itself.)

like image 38
Brooks Moses Avatar answered Sep 17 '22 15:09

Brooks Moses