C++: Using std::common_type

Another useful nugget buried in the standard template library.

bob kerner
6 min readAug 23, 2024

It’s always a joy to rummage through the C++ standard template library and come across useful little nuggets you didn’t know were there. One of those is std::common_type and its relatives defined in <type_traits> , so I thought I share in a quick and dirty blog post.

What does it do?

Well, it’s pretty straightforward. std::common_type will tell you what the “common type” between N different types is, if a common type exists at all. Two types T and U are said to be common if and only if T can be converted to the common type V identically (without losing information) and U can be similarly converted to V. For numerics, the common types are predefined:

  • std::common_type_t<int,double> => double
  • std::common_type_t<int, short> => int

std::common_type_t<…,…>is a helper here, by the way. It is just the type alias std::common_type<…,…>::type. You are not allowed to redefine the predefined types according to the standard. However, you are allowed to define a specialization of std::common_type if at least one of the types being compared is a user-defined type.

For completeness, I should mention that it can determine the common type of several types, as in std::common_type_t<int,float,double>.

Seems pretty simple, so what?

It’s not Earth-shattering, but it can come in handy in certain circumstances. Originally, std::common_type was defined for the chrono library duration types, but it turned out to be useful in other places, so it was promoted.

Here’s a contrived, generic example. Suppose we have a requirement to box numeric types, i.e., wrap them in a struct for some reason. Let’s define our Box, but first we’re going to constrain our Box type to only allow parameterization with arithmetic types:

// Define a concept to constrain types to arithemtic types, e.g., int, double, etc.
template<typename T> concept Arithmetic = std::is_arithmetic_v<T>;

We’ll use our concept in the definition of our template struct, Box:

template<Arithmetic T> //Allow parameterization with arithmetic types only.
struct Box{
const T t; //Contains the numeric value.

//Construct a box with a compatible box type.
template<Arithmetic U>
requires std::common_with<T,U> // Make sure T and U share a common type.
constexpr Box(const Box<U>& u) : t(u.t) { /* Do nothing. */ }

// Simple constructor.
constexpr Box(Arithmetic auto u) noexcept : t(u) { /* Do nothing. */ }

// Cast from Box to T.
[[nodiscard]] constexpr operator const T() const noexcept { return t; }

// Add two boxes of type T.
[[nodiscard]] constexpr Box<T> operator+ (const Box<T>& b) const {
return Box (t + b.t);
}
};

// We'll need this later to verify the type and value of our result.
template<typename T>
std::ostream& operator<<(std::ostream& os, const Box<T>& b){
os<< std::format("Box<{}> [with t = {}] ", type_name<T>(), b.t);
return os;
}

In this example, we’ll define an add method that adds two operands of like types and another to add two operands of different types. Our second method will use std::common_type to determine the common type between operands and seamlessly cast or box the values if (and only if) necessary. In our contrived requirement, boxing happens on an operand only if the other operand is boxed.

// Simple addition of two values of the same type. Works as long
// as the addition operator is defined for type T.
template<typename T>
T add(T a, T b) { return a + b; }

// Define addition for two different types and return a value of
// the common type.
// Notice that U is constrained to be a type "common with" T.
template<typename T, std::common_with<T> U>
requires std::common_with<T,U> // Not necessary, becuase std::common_with
// already guarantees that case.
auto add(T a, U b) -> std::common_type_t<T,U> {
// Here, we define an alias V that represents the common type between
// T and U.
using V = std::common_type_t<T,U>;

// Here, we assign the address of the add function that would take
// two V parameters and return a result of type V.
V(*fn_ptr)(V,V) = add<V>;

// Call the 'add' function that takes two operands of the same type,
// constructing two operands of type 'V' from 'a' and 'b'.
return fn_ptr(a,b);
}

In the code above, two addition functions are defined. One takes operands of the same type, and the other takes operands of differing types. We use std::common_with<T> U to constrain the second operand type to be a type where a common type exists with T. std::common_with<T> U will use our specialization of std::common_type to determine whether T is common with U.

I think it’s important to note that a function pointer was necessary to resolve which add function should be called. If that function pointer weren’t used and add<V>(a,b) were called, the function call would STILL resolve to the function with differing parameters, the one we are already in, even though we specifically called out add that is instantiated by only one parameter V. The function pointer is not strictly necessary. You could static_cast<V>(a) and static_cast<V>(b) before calling add. That would work too.

Finally, we add a boxed 5 with a float having the value 9.0f:

int main() {
std::cout << add( Box(5), 9.0f ) << '\n';
}

// Output: Box<float> [with t = 14]
// (The common type between Box<int> and float is defined to be Box<float>.)

The Cool Part

The feature that enabled us to do this is std::common_type. We had to specialize the std::common_type for our user-defined Box, but this is a legitimate thing to do according to the standard.

In this regard, we were not required to define overloads (internal and external) for our Box class. Everything gets converted to the common box type and our single operator+ method is invoked!

// Secialize the standard template std::common_type with our Box<T> type.
// The common type between boxes is a Box instantiated with the common type of each
// Box's parameterized type.
template<Arithmetic U, Arithmetic V>
struct std::common_type<Box<U>, Box<V>>{
using type = Box<std::common_type_t<U,V>>;
};

// The common type between a Box<U> and an Arithmetic type V is
// Box<X>, where X is the common type between U and V.
template<Arithmetic U, Arithmetic V>
struct std::common_type<V,Box<U>> {
using type = std::common_type_t<Box<V>,Box<U>>;
};

// The common type between a Box<U> and an Arithmetic type V is
// Box<X>, where X is the common type between U and V.
template<Arithmetic U, Arithmetic V>
struct std::common_type<Box<U>,V> {
using type = std::common_type_t<Box<V>,Box<U>>;
};

The first specialization, with std::common_type<Box<U>, Box<V>> is the one that the others will be based from. It defines the common type of two boxed arithmetic type to be the boxed type of the common arithmetic type between U and V.

The second and third specializations define the common type between any arithmetic type and a boxed arithmetic type. Both are required to fully define the relationship and ensure that std::common_with works. It is a prerequisite for the compilation of std::common_with.

The code

The code can be found here: https://godbolt.org/z/9YjsPhczc

I encourage you to play around and discover some more useful applications of std::common_type!

Redux

If you were wondering why I was using constexpr with the Box constructors, it’s because if there’s no reason not to, you should. It allows you to use your types at compile time, saving space and improving performance.

I’ve also started making a practice of using [[nodiscard]] everywhere that makes sense. It’s already saved me a couple times. I’m a believer.

What about noexcept? You should be generously using that if you are certain there won’t be any exceptions generated by your method. The compiler may be able to do some additional optimizations. No reason not to if you’re certain you’re not expecting any exceptions.

--

--

No responses yet