Search code examples
c++c++14

How to name dependent return type of template


Using std::enable_if or std::conditional we can create template functions whose return type depends on their template parameters, as per the std::enable_if example:

template<class T>
typename std::enable_if<std::is_trivially_default_constructible<T>::value>::type 
construct(T*) 
{
    std::cout << "default constructing trivially default constructible T\n";
}

(shortened for brevity)

How can we define a typename usable in the function body for this return type?

Unfortunately, as cautioned in the notes of the same article, we cannot just put the conditional expression into a named template parameter:

default template arguments are not accounted for in function template equivalence

/* WRONG */
 
struct T
{
    enum { int_t, float_t } type;
 
    template<typename Integer,
             typename = std::enable_if_t<std::is_integral<Integer>::value>>
    T(Integer) : type(int_t) {}
 
    template<typename Floating,
             typename = std::enable_if_t<std::is_floating_point<Floating>::value>>
    T(Floating) : type(float_t) {} // error: treated as redefinition
};

We could of course copy the definition into a using-directive in the first line of the function body, but then we would have to maintain both definitions - and nested std::conditional_t are not exactly the pinnacle of legibility (just way, way better than old school CRTP).

Is there a way to name the type without duplicating its definition?


Solution

  • The example you linked and took inspiration from aims at enabling a function using SFINAE (Substitution Failure Is Not An Error) on the return type. std::enable_if, if not instructed otherwise, resolves to a type that, if the substitution succeeded, contains a type field which is an alias for the void type. In essence, the return type of the example function is just void, if the substitution succeeds, otherwise the function gets discarded and compilation proceeds (SFINAE).

    Reading your question, I had understood you want to replicate that behavior on your class constructors, that is enable/disable them according to the type of the constructor's argument, in which case the syntax that would make it work could be the following.

    #include <type_traits>
    
    struct T
    {
        enum { int_t, float_t } type;
     
        template<typename Integer,
                 std::enable_if_t<std::is_integral_v<Integer>>* = nullptr>
        T(Integer) : type(int_t) {}
     
        template<typename Floating,
                 std::enable_if_t<std::is_floating_point_v<Floating>>* = nullptr>
        T(Floating) : type(float_t) {}
    };
    

    std::enable_if_t<std::is_floating_point_v<Floating>>* will expand to void* if the substitution succeeds, and therefore the template parameter will be pointer to void, that is a "non-type" parameter, which we default to nullptr. It works, compared to your previous example, because the non-type template parameter could have any other value, whilst void would be the same in both of your constructors in your attempt, hence the compiler would see that as a redefinition.

    But by reading your comments to your own question, I think I understood what you really want to achieve.

    The following function, named the way you named it in your comment, has a return type with the name Franky that depends on the first template parameter (which is also the type of the function argument itself) and can be freely used within the function's body.

    template <typename iTellYouWhetherFrankyIsAnIntegral, 
              typename Franky = std::conditional_t<std::is_integral_v<iTellYouWhetherFrankyIsAnIntegral>, uint32_t, float>>
    Franky theFrankyFunction(iTellYouWhetherFrankyIsAnIntegral v) {
        return Franky();
    }