g++ and clang++ different behaviour with variable template and SFINAE
Asked Answered
S

1

6

Another question of type "who's right between g++ and clang++?" for C++ standard gurus.

Suppose we want apply SFINAE to a variable template to enable the variable only if the template type fulfill a certain condition.

By example: enable bar if (and only if) the template type has a foo() method with a given signature.

Using SFINAE through an additional template type with default value

template <typename T, typename = decltype(T::foo())>
static constexpr int bar = 1;  

works for both g++ and clang++ but has a problem: can be hijacked explicating the second template type

So

int i = bar<int>;

gives a compilation error where

int i = bar<int, void>;

compile without problem.

So, from the bottom of my ignorance about SFINAE, I've tried enabling/disabling the type of the same variable:

template <typename T>
static constexpr decltype(T::foo(), int{}) bar = 2; 

Surprise: this works (compile) for g++ but clang++ doesn't accept it and give the following error

tmp_003-14,gcc,clang.cpp:8:30: error: no member named 'foo' in 'without_foo'
static constexpr decltype(T::foo(), int{}) bar = 2;
                          ~~~^

The question, as usual, is: who's right ? g++ or clang++ ?

In other words: according the C++14 standard, SFINAE can be used over the type of a variable template ?

The following is a full example to play with

#include <type_traits>

// works with both g++ and clang++
//template <typename T, typename = decltype(T::foo())>
//static constexpr int bar = 1;

// works with g++ but clang++ gives a compilation error
template <typename T>
static constexpr decltype(T::foo(), int{}) bar = 2;

struct with_foo
 { static constexpr int foo () { return 0; } };

struct without_foo
 { };

template <typename T>
constexpr auto exist_bar_helper (int) -> decltype(bar<T>, std::true_type{});

template <typename T>
constexpr std::false_type exist_bar_helper (...);

template <typename T>
constexpr auto exist_bar ()
 { return decltype(exist_bar_helper<T>(0)){}; }

int main ()
 {
   static_assert( true == exist_bar<with_foo>(), "!" );
   static_assert( false == exist_bar<without_foo>(), "!" );
 }
Sunglass answered 10/2, 2018 at 13:56 Comment(7)
Are you sure your first snippet runs sfinae and not hard error? In case of function templates it wouldn't...Violist
I hope T::foo() doesn't have a return type R with an overloaded operator,(R,int)...Frail
I mean template <class T, decltype(T::foo ()) * = nullptr> would invoke sfinae this one I'm not so sure...Violist
@Violist - You're making me come doubts. Not sure to understand your question. Do you mean: can we define it SFINAE if there isn't an alternative? In this case, yes, I suppose we can define another, specular, variable bar that is defined if (and only if) the first one isn't defined.Sunglass
@Frail - yes, I hope the same. I admit: my example isn't really perfect.Sunglass
@Sunglass yes I'm not sure about template variables either just trying to generalize by finding the common aspects...Violist
The absence of a clear definition of "immediate context" strikes again. Nobody knows for sure. (See core issue 1844.)Jameyjami
R
-1

Let's analyze what's happening here:

My initial assumption is that this looks like a clang missinterpretation. It isn't able to go back in the resolution tree when bar is not resolved properly.

First, to be sure that the problem is in bar, we can do this:

template <typename T>
constexpr auto exist_bar_helper(int) -> decltype(void(T::foo()), std::true_type{});

It works properly (SFINAE is doing its job).

Now, let's change your code to check if nested failed resolutions are wrapped by outer SFINAE context. After changing bar to be a function:

template <typename T>
static constexpr decltype(void(T::foo()), int{}) bar();

It still works properly, cool. Then, I would assume that any incorrect resolution inside our decltype will go back and make the function resolve to the SFINAE fallback (std::false_type)... but nope.

This is what GCC does:

exist_bar -> exists_bar_helper -> bar (woops) -> no worries, i have alternatives
          -> exists_bar_helper(...) -> false

This is what CLANG does:

exist_bar -> exists_bar_helper -> bar (woops) // oh no
// I cannot access that var, this is unrecoverable error AAAAAAAA

It takes it so seriously that forgets about the fallback in the upper context.

Long story short: don't SFINAE on templated variables, SFINAE its a compiler hack by itself and can behave in strange ways when compilers try to be "too smart"

Redmer answered 12/2, 2018 at 13:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.