2

I have the following code, see also live example:

template <typename A, typename B>
using ternary = decltype(true ? std::declval <A>() : std::declval <B>());

template <template <typename, typename> class F>
struct test
{
    template <typename A, typename B, typename T = F <A, B> >
    static std::true_type call(int);

    template <typename A, typename B>
    static std::false_type call(...);
};

template <template <typename, typename> class F, typename A, typename B>
using sfinae = decltype(test <F>::template call <A, B>(0));

template <typename T> struct X { };

template <typename T> struct Y
{
    template <typename A>
    Y(A&& a) { }
};

int main ()
{
    using A = X <int>;
    using B = X <double>;
    using C = Y <int>;
    using D = Y <double>;
    sfinae <ternary, A, B>{};  // OK
    sfinae <ternary, C, D>{};  // GCC error:
           // operands to ?: have different types ‘Y<int>’ and ‘Y<double>’
}

It's the result of extreme simplification of actual code, so don't ask if it's useful. Roughly, sfinae <ternary, A, B> does a pretty much standard SFINAE test on whether one may apply the ternary operator ?: to two arguments of types A, B.

Clang compiles fine. GCC is OK with the first call using class template X but gives an error on the second call using Y. The error indicates that SFINAE fails and the compiler unexpectedly tries to apply the ternary operator, which it shouldn't. SFINAE should never fail (result in a compiler error): it should always compile, returning either true_type or false_type (in this example, it should always return false_type).

The only difference between class templates X, Y is that Y has constructor

template <typename A>
Y(A&& a) { }

In my actual code, there are more constructors and all are equipped with enable_if to allow for disambiguation. To be just a bit more realistic, Y would be

template <typename T> struct Y
{
    using type = T;

    template <
       typename A,
       typename = typename std::enable_if <
          std::is_constructible<T, typename A::type>{}
       >::type
    >
    Y(A&& a) : /*...*/ { }
};

to allow construction by other Y's having different T, but nothing would change with respect to the error. Normally the ternary operator should either fail due to different types or be ambiguous due to the generic constructor (which is the case?) but either way SFINAE should report false_type.

How does this constructor make SFINAE fail? Is GCC compliant here? Any workaround?


EDIT

If I manually instantiate

decltype(true ? std::declval <C>() : std::declval <D>());

in main(), clang says

error: conditional expression is ambiguous; 'Y<int>' can be converted to 'Y<double>'
       and vice versa

while for A, B it says

incompatible operand types ('X<int>' and 'X<double>')

and GCC insists on the same error as above in both cases. I don't know if this may help.

iavr
  • 7,547
  • 1
  • 18
  • 53
  • For non-failure combinations, [std::common_type](http://en.cppreference.com/w/cpp/types/common_type) is guaranteed to provide the same result type as the ternary operator. Perhaps you could simplify your code using it and effectively work around the questionable GCC behaviour. – Tony Delroy Mar 26 '14 at 02:07
  • @TonyD Well, I didn't give much background, but for a [particular reason](http://stackoverflow.com/questions/21975812/should-stdcommon-type-use-stddecay), this code **is** in fact part of an alternative implementation of a slight variant of `std::common_type`, and uses the ternary operator exactly as `std::common_type` does. The issue is that at some point I want to know **if** I can apply the operator and this fails in this particular case. – iavr Mar 26 '14 at 02:12
  • It doesn't look like SFINAE. Could you plese eleborate why this substitution should fail? – AlexT Mar 26 '14 at 02:29
  • why do you use parameter pack notation (typename... A) here? You have only two template arguments – AlexT Mar 26 '14 at 02:36
  • @TonyD Sorry I don't follow. Which part doesn't look like SFINAE? Which substitution should fail? – iavr Mar 26 '14 at 02:36
  • @AlexT I used a variadic implementation in the original question because this is my generic code (to be applied to other predicates apart from `ternary`). But when [Potatoswatter](http://stackoverflow.com/a/22650007/2644390) stumbled on another GCC 4.9.0 bug, I edited the question and changed everything to binary. There should be no `typename... A` now, right? – iavr Mar 26 '14 at 02:40
  • @iavr I don't understand why do you expect SFINAE effect here. SFINAE means that compiler doesn't instantiate template which it failed to substitute. In your example I don't see where it can fail to do substitution. Both types exist and ternary expression requires both branches to have the same (similar) type which is not the case. I'm not really experienced with SFINAE so I'm curious what substitition you expected to fail? – AlexT Mar 26 '14 at 09:27
  • @AlexT Well, the rules are no that simple and that's exactly why we need `?:`. It's not just about same types, it could be types in the same hierarchy with a common ancestor, references and `const`/`volatile` qualifiers are also handled, plus implicit conversions etc. Here types are different and `ternary` should fail to substitute because there is no common type, so by SFINAE it should **not** be instatiated; but GCC does instantiate it, hence the error. – iavr Mar 26 '14 at 09:35
  • what if you try `template using ternary = typename decay_t() : std::declval ())>::type;` – AlexT Mar 26 '14 at 09:59
  • @AlexT You mean `std::decay`. This is even more certain to fail because it instantiates class `std::decay` with the invalid template argument. Class template instantiations are not allowed in SFINAE, only alias sustitutions. Anyhow, it *does* fail as well. Besides, if you read [here](http://stackoverflow.com/questions/21975812/should-stdcommon-type-use-stddecay), it is exactly `std::decay` that I'm trying to *avoid*. – iavr Mar 26 '14 at 10:26
  • @AlexT I've edited the question, and added some more information at the bottom, which may help (or not). – iavr Mar 26 '14 at 10:37

2 Answers2

1

This sounds a lot like a GCC bug. Using a fairly recent build of GCC 4.9 yields this error instead:

sftern.cpp: In instantiation of ‘struct test<ternary>’:
sftern.cpp:17:57:   required by substitution of ‘template<template<class ...> class F, class ... A> using sfinae = decltype (test:: call<A ...>(0)) [with F = ternary; A = {X<int>, X<double>}]’
sftern.cpp:33:26:   required from here
sftern.cpp:10:27: error: pack expansion argument for non-pack parameter ‘A’ of alias template ‘template<class A, class B> using ternary = decltype ((true ?  declval<A>() : declval<B>()))’
     static std::true_type call(int);
                           ^
sftern.cpp:3:11: note: declared here
 template <typename A, typename B>
           ^

It sounds like the bug you describe has been fixed, but it's still getting tripped up. (There's no problem with expanding a pack into a non-pack, as long as the number of items is correct.)

Potatoswatter
  • 134,909
  • 25
  • 265
  • 421
  • Thanks. This is in fact a [different bug](http://stackoverflow.com/questions/20291352/variadic-template-aliases-as-template-arguments-part-2) of GCC that breaks out before coming to the error of this question. I will try to update the code in the question to make this go away, *then* you'll see the actual error. – iavr Mar 26 '14 at 01:56
  • Code updated: variadic templates switched to binary. This is not generic now but fits this example and should bypass the error you got here. – iavr Mar 26 '14 at 02:01
  • @iavr Now GCC compiles it without complaint. The bug in question has been fixed. – Potatoswatter Mar 26 '14 at 02:10
  • Oh, that's great news on one hand, thanks! On the other hand, upgrading GCC just for this is a huge change because I know 4.9.0 has other issues like the one you mentioned, plus it's much slower. I'd rather stay with 4.8.x, so if you have any idea for a workaround, it's welcome :-) – iavr Mar 26 '14 at 02:25
0

I've slightly rephrased this example and I've started getting errors in clang:

template <typename A, typename B>
struct ternary1
{
    typedef decltype(true ? std::declval <A>() : std::declval <B>()) type;
};

template <template <typename, typename> class F>
struct test1
{
    template <typename A, typename B, typename T = typename F <A, B>::type >
    static std::true_type call(int);

    template <typename A, typename B>
    static std::false_type call(...);
};

template <template <typename, typename> class F, typename A, typename B>
using sfinae1 = decltype(test1 <F>::template call <A, B>(0));

template <typename T> struct X { };

template <typename T> struct Y
{
    template <typename A>
    Y(A&& a) { }
};

int main ()
{
    using A = X <int>;
    using B = X <double>;
    using C = Y <int>;
    using D = Y <double>;
    sfinae <ternary, A, B>{}; //clang: Incompatible operand types ('X<int>' and 'X<double>')
    sfinae <ternary, C, D>{}; //clang: Conditional expression is ambiguous; 'Y<int>' can be converted to 'Y<double>' and vice versa
}

Is ternary equivalent to typename ternary1::type or I'm missing something?

AlexT
  • 1,413
  • 11
  • 11