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?
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();
}