Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I create a constexpr function that returns a type (to be used in a template parameter)

Tags:

c++

I'm looking for some way to create a class, with a template parameter type, based on a template parameter number.

What I'm trying to do is something like this:

template<size_t n>
constexpr auto type_from_size() {
    if(n < 256) {
        return uint8_t;
    } else {
        return uint16_t;
    }
}

template<size_t n>
class X {
    type_from_size<n>() t;
}

X<500> x;
x.t = 500;

So, in the code above, the constexpr function type_from_size() would receive the number 500 and would return the type uint16_t, and this would be the type of the member X.t.

I know this is obviously terrible code, but is this possible using templates?

like image 595
André Wagner Avatar asked May 27 '16 14:05

André Wagner


People also ask

Can a function return constexpr?

A constexpr function is a function that can be invoked within a constant expression. A constexpr function must satisfy the following conditions: It is not virtual. Its return type is a literal type.

Can a function parameter be constexpr?

We allow annotating a function parameter with constexpr with the same meaning as a variable declaration: must be initialized with a constant expression.

What is static constexpr in C++?

Static specifies the lifetime of the variable. A static constexpr variable has to be set at compilation, because its lifetime is the the whole program. Without the static keyword, the compiler isn't bound to set the value at compilation, and could decide to set it later.

Can constexpr functions call non constexpr functions?

A call to a constexpr function produces the same result as a call to an equivalent non- constexpr function , except that a call to a constexpr function can appear in a constant expression. The main function cannot be declared with the constexpr specifier.


2 Answers

A function cannot return a type. You should use a template.

For a selection between only two types, the built-in std::conditional is sufficient.

#include <type_traits>
#include <cstdint>

template <size_t n>
using type_from_size = typename std::conditional<(n < 256), uint8_t, uint16_t>::type;
// ^ if `n < 256`, the ::type member will be typedef'ed to `uint8_t`.
//                 otherwise, it will alias to `uint16_t`.
//   we then give a convenient name to it with `using`.

template <size_t n>
struct X {
    type_from_size<n> t;
    // ^ use the template
};

If you need to support more than two values, you can change multiple conditional together like an if/else if/else chain, but OH MY EYES

template <size_t n>
using type_from_size =
    typename std::conditional<(n <= 0xff), uint8_t,
        typename std::conditional<(n <= 0xffff), uint16_t,
            typename std::conditional<(n <= 0xffffffff), uint32_t,
                uint64_t
            >::type
        >::type
    >::type;

You could also use specialization together with std::enable_if (SFINAE) to make it more "low-level":

template <size_t n, typename = void>
struct type_from_size_impl;
// Declare a "size_t -> type" function.
//  - the `size_t n` is the input
//  - the `typename = void` is a placeholder
//    allowing us to insert the `std::enable_if` condition.

template <size_t n>
struct type_from_size_impl<n, typename std::enable_if<(n <= 0xff)>::type> {
    using type = uint8_t;
};
// We add a partial specialization
//  - in `std::enable_if<c>::type`, if `c` is true, `::type` will be typedef'ed to `void`
//  - otherwise, `::type` will not be defined.
//  - if `::type` is not defined, substitution failed,
//    meaning we will not select this specialization

template <size_t n>
struct type_from_size_impl<n, typename std::enable_if<(n > 0xff && n <= 0xffff)>::type> {
    using type = uint16_t;
};

template <size_t n>
struct type_from_size_impl<n, typename std::enable_if<(n > 0xffff && n <= 0xffffffff)>::type> {
    using type = uint32_t;
};

template <size_t n>
struct type_from_size_impl<n, typename std::enable_if<(n > 0xffffffff)>::type> {
    using type = uint64_t;
};

template <size_t n>
using type_from_size = typename type_from_size_impl<n>::type;
// Here we want to find a specialization of `type_from_size_impl<n>`
// All 4 specializations will be tried.
// If only one specialization works, we will use that one
// (Which is why we need to ensure the ranges are not overlapping
//  otherwise the compiler will complain)
// Then we take the `::type` out the complete this "type-level function".
like image 171
kennytm Avatar answered Oct 06 '22 22:10

kennytm


Definitely. Here's a more flexible way of doing it, you can add as many ranges as you wish as long as they don't overlap.

template <std::size_t N, class = void>
struct TypeForSize_;

template <std::size_t N>
struct TypeForSize_<N, std::enable_if_t<
    (N <= 255)
>> { using type = std::uint8_t; };

template <std::size_t N>
struct TypeForSize_<N, std::enable_if_t<
    (N > 255 && N <= 65535)
>> { using type = std::uint16_t; };

template <std::size_t N>
using TypeForSize = typename TypeForSize_<N>::type;

Using a size for which no type has been defined will result in a compile-time error.

like image 41
Quentin Avatar answered Oct 06 '22 21:10

Quentin