1

Lets take something concrete:

#include <utility>
#include <vector>

template <typename ... Ts>
using void_t = void;

template <typename T, typename = void_t<>>
struct is_lt_comparable : std::false_type {};

template <typename T>
struct is_lt_comparable<T, void_t<decltype(std::declval<T>() < std::declval<T>())>> : std::true_type {};                    

template <typename T>
static constexpr bool is_lt_comparable_v = is_lt_comparable<T>::value;

struct test{};

int main()
{
    #define error_message "detection doesn't work correctly"
    static_assert(is_lt_comparable_v<int>, error_message);
    static_assert(!is_lt_comparable_v<test>, error_message);
    static_assert(is_lt_comparable_v<std::vector<int>>, error_message);

}

Wandbox.

In the code above, why doesn't first and the last asserts trigger double definition of is_lt_comparable?

void_t with any arguments is still void. As such, the last unnamed parameter for the template is always void. IIRC type aliases are not considered distinct types, thus my intuition leads me to believe that I'm missing something.

Specifically, given a choice that both declaration are valid, and result in the same type, e.g. in the first one, is_lt_comparable<int, void>, how does it know which template to instantiate?

Incomputable
  • 2,188
  • 1
  • 20
  • 40
  • This might as well be named "Why detection idiom works", but I wasn't sure if it is a good title. – Incomputable Jan 01 '18 at 16:42
  • Do you understand that in this snippet the second appearance `template struct is_lt_comparable` designates a *template specialization*? – HolyBlackCat Jan 01 '18 at 16:44
  • @HolyBlackCat, yes, but why it is not treated the same as first appearance? – Incomputable Jan 01 '18 at 16:45
  • @Incomputable You might want to have a look at https://stackoverflow.com/questions/44858395/why-is-the-template-specialization-not-chosen/44858464#44858464 (not really a dupe though) Maybe it'll help. – Rakete1111 Jan 01 '18 at 16:46
  • 1
    The first one is not a specialization, it's the struct template itself. If a type is comparable, the specialization is preferred over the original struct because that's how they work. If it's not comparable, SFINAE discards the specialization and the original struct is used. – HolyBlackCat Jan 01 '18 at 16:46
  • Are you asking how a specialization is different from a plain template? It has a pair of `<>` with some arguments after a (struct) name. – HolyBlackCat Jan 01 '18 at 16:50
  • @HolyBlackCat, no. I understand the case when it is not comparable. I'm not getting the case when it *is* comparable. It is still `is_lt_comparable`, it is able to instantiate both. How does it figure out which one to instantiate? The first one? – Incomputable Jan 01 '18 at 16:51
  • 1
    A (most specialized) specialization is always used when possible. The original doesn't need to be discarded by SFINAE or anything. – HolyBlackCat Jan 01 '18 at 16:54
  • @HolyBlackCat, so it is about precedence? Like in function overload resolution? – Incomputable Jan 01 '18 at 16:54
  • I guess it's similar in a sense. – HolyBlackCat Jan 01 '18 at 16:55
  • It seems that your question can be reduced to: `template struct S {static constexpr int x = 1;};` `template <> struct S {static constexpr int x = 2;};` `std::cout << S::x; // 2` – HolyBlackCat Jan 01 '18 at 16:56
  • @HolyBlackCat, yes, exactly. Close as a dupe, I guess? – Incomputable Jan 01 '18 at 16:57
  • ¯\\_(ツ)_/¯ I can't find a proper dupe target. – HolyBlackCat Jan 01 '18 at 16:59

3 Answers3

4

You write: is_lt_comparable<int>. This is what happens.

  1. The primary template is chosen, and the second template argument is inferred, because it's the default. So, you actually have is_lt_comparable<int, void>.

  2. Now the template specializations are considered, to see if there is a match.

  3. It finds the first (and only) specialization, and because it is a partial specialization, not a full one, it needs to instantiate it too, basically. So you get:

    is_lt_comparable<int, void_t<decltype(std::declval<int>() < std::declval<int>())>>
    

    Now, if the < expression is ill-formed, then the specialization is not considered, and the compiler fall backs to the primary template.

  4. But if it is valid, then the partial specialization becomes: is_lt_comparable<int, void>. And that is an exact match from the template that we instantiated in 1), so the compiler chooses that one. Formally, this is known as the partial ordering rules.

If you are still confused, consider this:

template<typename> void foo() {}
template<> void foo<int>() {}

If I do foo<int>(), there also won't be a double definition error, like you said there'd be. The specialization is a better match than the primary, so the compiler doesn't even instantiate the primary template with T = int (it can't).

Rakete1111
  • 47,013
  • 16
  • 123
  • 162
  • This was the explanation I was searching for. I would like my question to be closed as a dupe if possible, but it seems like there is no proper dupe target. Should I edit the question to better fit the answer? – Incomputable Jan 01 '18 at 17:02
  • @Incomputable Please don't. That would invalidate mine and the other answers – Rakete1111 Jan 01 '18 at 17:04
1

A static_assert is a declaration and doesn't define any entities. See cpprefrence article on Definitions and ODR. There are only template instantiations in your code, but no definitions.

jrok
  • 54,456
  • 9
  • 109
  • 141
0

In the code above, why doesn't first and the last asserts trigger double definition of is_lt_comparable?

is_lt_comparable is not a type. It's the name of a template.

is_lt_comparable_v<int> results in expansion of is_lt_comparable<int, void>, which is a type.

is_lt_comparable_v<test> results in the expansion of is_lt_comparable<test, void>, which is a different type.

And once again, is_lt_comparable<std::vector<int>, void> is yet another distinct type.

Richard Hodges
  • 68,278
  • 7
  • 90
  • 142