23

I recently upgraded GCC to 8.2, and most of my SFINAE expressions have stopped working.

The following is somewhat simplified, but demonstrates the problem:

#include <iostream>
#include <type_traits>

class Class {
public:
    template <
        typename U,
        typename std::enable_if<
            std::is_const<typename std::remove_reference<U>::type>::value, int
        >::type...
    >
    void test() {
        std::cout << "Constant" << std::endl;
    }

    template <
        typename U,
        typename std::enable_if<
            !std::is_const<typename std::remove_reference<U>::type>::value, int
        >::type...
    >
    void test() {
        std::cout << "Mutable" << std::endl;
    }
};

int main() {
    Class c;
    c.test<int &>();
    c.test<int const &>();
    return 0;
}

C++ (gcc) – Try It Online

C++ (clang) – Try It Online

Older versions of GCC (unfortunately I don't remember the exact version I had installed previously) as well as Clang compile the above code just fine, but GCC 8.2 gives an error stating:

 : In function 'int main()':
:29:19: error: call of overloaded 'test()' is ambiguous
     c.test();
                   ^
:12:10: note: candidate: 'void Class::test() [with U = int&; typename std::enable_if::type>::value>::type ... = {}]'
     void test() {
          ^~~~
:22:10: note: candidate: 'void Class::test() [with U = int&; typename std::enable_if::type>::value)>::type ... = {}]'
     void test() {
          ^~~~
:30:25: error: call of overloaded 'test()' is ambiguous
     c.test();
                         ^
:12:10: note: candidate: 'void Class::test() [with U = const int&; typename std::enable_if::type>::value>::type ... = {}]'
     void test() {
          ^~~~
:22:10: note: candidate: 'void Class::test() [with U = const int&; typename std::enable_if::type>::value)>::type ... = {}]'
     void test() {

As is usually the case when different compilers and compiler versions handle the same code differently I assume I am invoking undefined behavior. What does the standard have to say about the above code? What am I doing wrong?


Note: The question is not for ways to fix this, there are several that come to mind. The question is why this doesn't work with GCC 8 - is it undefined by the standard, or is it a compiler bug?

Note 2: Since everyone was jumping on the default void type of std::enable_if, I've changed the question to use int instead. The problem remains.

Note 3: GCC bug report created

zennehoy
  • 6,405
  • 28
  • 55
  • 1
    Going by [godbolt](https://godbolt.org/g/qeS6vm), it worked as late as `gcc 7.3` (and you can see in the assembly that it does the correct thing). – Max Langhof Aug 10 '18 at 13:42
  • 6
    what are you tying to expand with the `...` after `::type` ? – Tyker Aug 10 '18 at 13:46
  • What does the ellipsis stands for here? If the condition is true, then there is effectively `template `. – Daniel Langr Aug 10 '18 at 13:46
  • Good point, that's a relic from simplifying the code. Replacing the default `void` with `int` causes the same effect. The original code uses a memberless enum to prevent accidentally specifying a second template paramter. – zennehoy Aug 10 '18 at 13:49
  • 1
    How about removing ellipsis? – Slava Aug 10 '18 at 13:54
  • 6
    Removing ellipsis, replacing the default `void` by `int` and adding a default fixes the problem, yes. The real question is, what is it about the above code that worked fine before, but doesn't work any more with GCC 8? – zennehoy Aug 10 '18 at 13:56
  • More simplified example of the same problem I think: https://wandbox.org/permlink/xFX6AzuqMB7GSa87. – Daniel Langr Aug 10 '18 at 14:17
  • Possible duplicate of [isn't non-type parameter pack that evaluates to "void..." illegal?](https://stackoverflow.com/questions/23401376/isnt-non-type-parameter-pack-that-evaluates-to-void-illegal) – xskxzr Aug 10 '18 at 14:17
  • @DanielLangr That one fails in the exact opposite gcc versions as the one in the question... – Max Langhof Aug 10 '18 at 14:21
  • @MaxLanghof Sure, but the cause should be the very same. – Daniel Langr Aug 10 '18 at 14:23
  • 2
    @xskxzr By that question, `void...` is legal for and only for empty parameter packs, which is exactly what is desired here. Even if it were illegal, replacing the default `void` with `int` in the `std::enable_if`doesn't change anything about the question. – zennehoy Aug 10 '18 at 14:28
  • 2
    Minimal example: https://godbolt.org/g/P9z1pt gcc7.1 OK gcc 8.x KO – YSC Aug 10 '18 at 14:56
  • funny, without the second definition: https://godbolt.org/g/RgwSVJ gcc7.1 KO, gcc 8.x OK – YSC Aug 10 '18 at 15:00
  • @YSC That's because `f` in your second example only exists for a constant template parameter. – zennehoy Aug 10 '18 at 15:03
  • This is weird: https://godbolt.org/g/mnWcg6 gcc7.1 OK, gcc 8.x OK – YSC Aug 10 '18 at 15:05
  • @zennehoy Yes, but `f` is called! – YSC Aug 10 '18 at 15:05
  • It reaaaaaly looks like a gcc bug. – YSC Aug 10 '18 at 15:06
  • @YSC Not quite, the attempt to call `f` results in an error, since `f` only exists for e.g. `f`. Also, the non-type `void...` template parameter pack is not the problem - feel free to replace the `std::enable_if` type from `void` to e.g. `int`. – zennehoy Aug 10 '18 at 15:08
  • @zennehoy _that's_ what strange: on gcc 8.1, `f()` succeeds. Look: https://godbolt.org/g/RgwSVJ – YSC Aug 10 '18 at 15:24
  • @YSC Ah yes, sorry, I was only looking at the 7.1 column. Something is definitely strange here... (Clang also gives an error as I would expect). – zennehoy Aug 10 '18 at 15:30
  • About that specific point, I asked a question here: https://stackoverflow.com/q/51789825/5470596 – YSC Aug 10 '18 at 15:35
  • 1
    Emm... I think [this post](https://stackoverflow.com/q/10377183/5376789) is a more proper duplicate, though new versions of the compilers interchange their behaviors, interesting... – xskxzr Aug 10 '18 at 15:57

3 Answers3

5

This is my take on it. In short, clang is right and gcc has a regression.

We have according to [temp.deduct]p7:

The substitution occurs in all types and expressions that are used in the function type and in template parameter declarations. [...]

This means that the substitution has to happen whether or not the pack is empty or not. Because we are still in the immediate context, this is SFINAE-able.

Next we have that a variadic parameter is indeed considered an actual template parameter; from [temp.variadic]p1

A template parameter pack is a template parameter that accepts zero or more template arguments.

and [temp.param]p2 says which non-type template parameters are allowed:

A non-type template-parameter shall have one of the following (optionally cv-qualified) types:

  • a type that is literal, has strong structural equality ([class.compare.default]), has no mutable or volatile subobjects, and in which if there is a defaulted member operator<=>, then it is declared public,

  • an lvalue reference type,

  • a type that contains a placeholder type ([dcl.spec.auto]), or

  • a placeholder for a deduced class type ([dcl.type.class.deduct]).

Note that void doesn't fit the bill, your code (as posted) is ill-formed.

Community
  • 1
  • 1
Rakete1111
  • 47,013
  • 16
  • 123
  • 162
  • The problem is not the `void` default type of `std::enable_if`! If I understand your answer correctly, SFINAE should work fine for a different type (e.g. `int`). It doesn't under GCC 8.2. – zennehoy Aug 10 '18 at 18:19
  • @zennehoy That's why I said in the second sentence "regression if not `void`" :) – Rakete1111 Aug 10 '18 at 18:19
  • Do you mind reducing your answer to that? I've specified `int` as the type for `std::enable_if` in the question now, since I really didn't intend to start the discussion of `void...`, which was originally a relic of my simplifying the code. Note that others come to a different conclusion than you btw: https://stackoverflow.com/a/23711944/694509 :) – zennehoy Aug 10 '18 at 18:34
  • @zennehoy I don't really understand the answer; thanks though – Rakete1111 Aug 10 '18 at 18:38
  • 1
    @zenn that other answer is incorrect, since this is not a pack expansion. It's merely a nonexpanding template parameter pack which, had you given it a name, could be expanded elsewhere. – Johannes Schaub - litb Aug 10 '18 at 19:06
1

I am not a language lawyer, but cannot the following quote be somehow connected to the problem?

[temp.deduct.type/9]: If Pi is a pack expansion, then the pattern of Pi is compared with each remaining argument in the template argument list of A. Each comparison deduces template arguments for subsequent positions in the template parameter packs expanded by Pi.

It seems to me that since there is no remaining argument in the template argument list, then there no comparison of the pattern (which contains enable_if). If there is no comparison, then there is also no deduction and substitution occurs after deduction I believe. Consequently, if there is no substitution, no SFINAE is applied.

Please correct me if I am wrong. I am not sure whether this particular paragraph applies here, but there are more similar rules regarding pack expansion in [temp.deduct]. Also, this discussion can help someone more experienced to resolve the whole issue: https://groups.google.com/a/isocpp.org/forum/#!topic/std-discussion/JwZiV2rrX1A.

Daniel Langr
  • 22,196
  • 3
  • 50
  • 93
  • Interestingly, that discussion comes to the conclusion that this was a bug in Clang that was subsequently fixed! There seems to be quite a bit of uncertainty about this in the standard... – zennehoy Aug 10 '18 at 15:33
0

Partial answer: use typename = typename enable_if<...>, T=0 with different Ts:

#include <iostream>
#include <type_traits>

class Class {
public:
    template <
        typename U,
        typename = typename std::enable_if_t<
            std::is_const<typename std::remove_reference<U>::type>::value
        >, int = 0
    >
    void test() {
        std::cout << "Constant" << std::endl;
    }

    template <
        typename U,
        typename = typename  std::enable_if_t<
            !std::is_const<typename std::remove_reference<U>::type>::value
        >, char = 0
    >
    void test() {
        std::cout << "Mutable" << std::endl;
    }
};

int main() {
    Class c;
    c.test<int &>();
    c.test<int const &>();
    return 0;
}

(demo)

Still trying to figure out what the heck does std::enable_if<...>::type... mean knowing the default type is void.

YSC
  • 38,212
  • 9
  • 96
  • 149
  • See https://stackoverflow.com/a/23711944/694509, basically it requires the parameter pack to be empty. Feel free to add `int` to replace the default `void` type - that doesn't change anything about the question. – zennehoy Aug 10 '18 at 14:48
  • @zennehoy Note: without the added tparam (`int` & `char`), your two templates functions have the same template signature, hence the ambiguity. What I don't get is why it ever worked before. – YSC Aug 10 '18 at 14:51
  • How so? One of them is `template ` and the other is SFINAE, i.e. `template `. – zennehoy Aug 10 '18 at 14:59
  • @YSC: `typename = std::enable_if_t` requires "strange" extra dummy parameter, better to use `std::enable_if_t = 0`. – Jarod42 Aug 10 '18 at 16:37