fudo
fudo

Reputation: 2890

c++20 std::integral_constant?

I'm just into metaprogramming and was watching Cppcon channel on youtube, and saw this std::integral_constant, but cannot find its use.

As far as i understood, it is a way to "pack" a value along with its type, it can be instantiated with

std::integral_constant<int, 42> i;
std::integral_constant<bool, true> b;
std::integral_constant<float, 3.14> f;
...

and each of those instances can be used just like the value they contains, i.e.: can be passed around, used in math operations, comparisons, etc.

But i cannot understand why should i use those structs instead of the actual contained values, nor if i can actually access the value type (i.e.: int, bool and float) at runtime to do stuff with it.

Can someone present a simple example of practical use of this feature? An example that explain its usage difference from using the actual values?

Upvotes: 22

Views: 3146

Answers (2)

Jarod42
Jarod42

Reputation: 217235

Can someone present a simple example of practical use of this feature? An example that explain its usage difference from using the actual values?

It is mostly to be used in generic (i.e template) code.

One practical usage is to transform runtime value to compile time value:

Given:

enum class E { A, B /*..*/ };

template <E e> void foo() {/*..*/}
template <E e> void bar() {/*..*/}

instead of

void foo(E e) {
    switch (e) {
        case E::A: return foo<E::A>();
        case E::B: return foo<E::B>();
        //..
    }
    std::unreachable();
}
void bar(E e) {
    switch (e) {
        case E::A: return bar<E::A>();
        case E::B: return bar<E::B>();
        //..
    }
    std::unreachable();
}

you might do:

std::variant<std::integral_constant<E, E::A>,
             std::integral_constant<E, E::B>
             // ..
>
to_integral_constant_var(E e) {
    switch (e) {
        case E::A: return std::integral_constant<E, E::A>();
        case E::B: return std::integral_constant<E, E::B>();
        //..
    }
    std::unreachable();
}
void foo(E e) { std::visit([](auto I){ foo<I>()}, to_integral_constant_var(e)); }
void bar(E e) { std::visit([](auto I){ bar<I>()}, to_integral_constant_var(e)); }

Another usage case is the possibility to overload instead of specialization:

So instead of

template <int I, int J>
void foo() { std::cout << "generic" << I << ", " << J << std::endl; }

// No partial specialization of method possible
// template <int J>
// void foo<42, J>() { std::cout << "partial special 42, " << J << std::endl; }

template <>
void foo<42, 42>() { std::cout << "full special 42 42\n"; }

you might have

// alias to shorten type
template <int N> using IntC = std::integral_constant<int, N>;

template <int I, int J>
void foo(IntC<I>, IntC<J>) { std::cout << "generic" << I << ", " << J << std::endl; }

template <int J>
void foo(IntC<42>, IntC<J>) { std::cout << "partial special 42, " << J << std::endl; }

void foo(IntC<42>, IntC<42>) { std::cout << "full special 42 42\n"; }

But I cannot understand why should I use those structs instead of the actual contained values, nor if I can actually access the value type

One case I see when you cannot use the value directly is with variadic template, as you cannot mix types and values, but the wrapping value in type allow to have that mix

template <typename T>
void foo(std::type_identity<T>)
{
    std::cout << std::source_location::current().function_name() << std::endl;
}
template <typename T, T Value>
void foo(std::type_identity<std::integral_constant<T, Value>>)
{
    std::cout << Value << std::endl;
}


template <typename... Ts>
void foos()
{
    (foo(std::type_identity<Ts>{}), ...);
}

Demo

Upvotes: 1

dfrib
dfrib

Reputation: 73186

std::integral_constant is mainly used as a utility type for writing meta-programming traits, particularly by encoding a type with a type and a value. By letting a custom trait inherit from specializations of std::integral_constant we get easy, idiomatic access to a stored non-type template parameter through the static member constant value, as well as e.g. the value type of this constant via the member typedef value_type.


Examples

std::integral_constant could be used for, say, writing a dimensionality trait for a matrix type

using index_t = int;
template <index_t M, index_t N, typename S> struct mat {
  // matrix implementation
};

as

#include <type_traits>

// Default dimensionality 0.
template <class T, typename = void>
struct dimensionality : std::integral_constant<index_t, 0> {};

template <typename S>
struct dimensionality<S, std::enable_if_t<std::is_arithmetic_v<S>>>
    : std::integral_constant<index_t, 1> {};

template <index_t M, index_t N, typename S>
struct dimensionality<mat<M, N, S>> : std::integral_constant<index_t, M * N> {};

template <class T>
inline constexpr index_t dimensionality_v = dimensionality<T>::value;

DEMO.

However, a more common use case is to use the two helper typedefs std::true_type and std::false_type for the common case where T is bool.

Type        Definition
----        ----------
true_type   std::integral_constant<bool, true>
false_type  std::integral_constant<bool, false>

E.g. as

#include <type_traits>

struct Foo {};

template <typename T>
struct is_foo : public std::false_type {};

template<>
struct is_foo<Foo> : public std::true_type {};

template<typename T>
constexpr bool is_foo_v = is_foo<T>::value;

static_assert(is_foo_v<Foo>, "");
static_assert(!is_foo_v<int>, "");

Upvotes: 15

Related Questions