cmannett85
cmannett85

Reputation: 22346

std::conditional compile-time branch evaluation

Compiling this:

template < class T, class Y, class ...Args >
struct isSame
{
    static constexpr bool value = std::conditional<
                                      sizeof...( Args ),
                                      typename std::conditional<
                                          std::is_same< T, Y >::value,
                                          isSame< Y, Args... >, // Error!
                                          std::false_type >::type,
                                      std::is_same< T, Y > >::type::value;
};

int main()
{
    qDebug() << isSame< double, int >::value;
    return EXIT_SUCCESS;
}

Gives me this compiler error:

error: wrong number of template arguments (1, should be 2 or more)

The issue is that isSame< double, int > has an empty Args parameter pack, so isSame< Y, Args... > effectively becomes isSame< Y > which does not match the signature.

But my question is: Why is that branch being evaluated at all? sizeof...( Args ) is false, so the inner std:conditional should not be evaluated. This isn't a runtime piece of code, the compiler knows that sizeof..( Args ) will never be true with the given template types.

If you're curious, it's supposed to be a variadic version of std::is_same, not that it works...

Upvotes: 6

Views: 6210

Answers (4)

Jarod42
Jarod42

Reputation: 217145

You have an error because the type has to be correct when used as template parameter.
You may use template specialization to solve your issue, something like:

#include <type_traits>

template <typename ... Ts> struct are_same;

template <> struct are_same<> : std::true_type {};
template <typename T> struct are_same<T> : std::true_type {};

template <typename T1, typename T2, typename... Ts>
struct are_same<T1, T2, Ts...> :
    std::conditional<
        std::is_same<T1, T2>::value,
        are_same<T2, Ts...>,
        std::false_type
    >::type
{};

static_assert(are_same<char, char, char>::value, "all type should be identical");
static_assert(!are_same<char, char, int>::value, "all type should not be identical");

Upvotes: 5

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275385

The problem is that conditional is just a template. Its type is dependent on all 3 parameters, even if ::type field is only dependent on 2 of them (the bool and whichever it picks).

To do what you want, pass it types that represent how to apply arguments to both sides.m Then pass the arguments to the result.

template<template<class...>class Target>struct defer{
  template<class...Ts>using execute=Target<Ts...>;
};
template<class T>struct sink{
  template<class...>using execute=T;
};
template< class Prog, class... Ts > using run=Prog::template execute<Ts...>;

using choice = typename std::conditional< test, defer<Foo>, sink< std::false_type >  >::type;
using result = run< choice, int, double >;

gives us a conditional that returns a deferd Foo, or a sinkd false_type. We can then run it with some arguments, giving us either Foo<int, double> or false_type.

sink's execute ignores the arguments and always returns the passed in type, while defer applies them to the passed in template.

The trick here is that Foo<int, double> is not run if test is false, because in that case choice is sink<false_type>!

template<bool b, class A, class B, class...Ts>
using pick = run< typename std::conditional< b, A, B >::type, Ts... >;

using result=pick<test, defer<Foo>, sink<std::false_type>, int, double >;

does the same as choice/result above in one line.

To solve your problem, we defer execution on one of your if branches:

static constexpr bool value =
typename std::conditional< sizeof...( Args ),
  pick< std::is_same< T, Y >::value,
    defer<isSame>,
    sink<std::false_type>,
    Y, Args...
  >,
  std::is_same< T, Y >
>::type::value;

now, there are often ways to avoid this technique, but you should know it exists.

Upvotes: 5

Filip Ros&#233;en
Filip Ros&#233;en

Reputation: 63797

Don't blame std::conditional

In short it's because the template argument substitution happens outside of std::conditional, which means that it's not std::conditional that is causing the error.. it's isSame.

Imagine std::conditional being a function call (which it isn't), what we pass as parameters are evaulated before the actual body of the function is evaluated, and what we pass as parameters sure must be a valid construct, even if the function itself doesn't use them.

Further reading is available in the below linked answer:


A solution

Add some indirection so that you don't instantiate isSame if sizeof... (Args) == 0, you could have a traited used as isSame_if_not_empty<Args..>::type which would yield isSame<Args...> if Args is not empty, and something else if it is indeed empty.


Proposed solution

Fix so that isSame can be used with an empty variadic pack, yielding true, this is the sensible approach. If the description of isSame is "all types passed are of the same type", an empty pack sure has "all of its types" of the same type.

Upvotes: 5

Manu343726
Manu343726

Reputation: 14174

But my question is: Why is that branch being evaluated at all?

Because there is no evaluation, and thats not a branch at all. Thats a template and its instanced to successfully instance the std::conditional template.
What I mean here is that the evaluation abstraction is usefull when writting template metaprograms, but you should never forget what the template system is really doing.

If you need to conditionally instantiate a potentially ill-formed template, add a level of indirection. Check this answer for an example of that.

Upvotes: 6

Related Questions