4

Problem

I wrote a convoluted piece of template code that can be compiled with GCC 8.2.1, but not with Clang 7.0 (code and error links).

I think this might be an implication of this Q&A, but I am unable to see it.

Motivation

I am writing a class, which I would like to be constructible with two callables of different types, but also with one of them omitted, i.e.:

my_class(callable_1);
my_class(callable_2);
my_class(callable_1, callable_2);

That should go without problems. But, why not allow callable_1 and callable_2 to be function templates (or functors with operator() template). That is, I would like to have this (or at least initially wanted):

my_class([](auto arg) {});
my_class([](auto arg) {});
my_class([](auto arg) {}, [](auto arg) {});

As, you can see, both callables unfortunately have the same signature, so we need to disambiguate between them somehow. The first approach I could think of (and the one this question is about) is to add a "tag" parameter to one of the unary overloads:

my_class([](auto arg) {});
my_class([](auto arg) {}, callable_2_tag());
my_class([](auto arg) {}, [](auto arg) {});

This, to me, looks acceptable, but I have come up with better solutions:

  • use the tag (optional if not ambiguous) in the second callable's signature (the last parameter or return type)
  • make the second constructor overload into a differently named non-member or static member function

Still, I would like to know, why there is the difference in behavior among the two compilers with my initial approach and which one is correct (or whether both are).


Code:

I have translated the constructor overloads into regular my_class function overloads for simplicity.

#include <iostream>
#include <type_traits>

// parameter types for callbacks and the tag class
struct foo { void func1() {} };
struct bar { void func2() {} };
struct bar_tag {};

// callable checks
template <typename Func>
static constexpr bool is_valid_func_1_v = std::is_invocable_r_v<void, Func, foo>;

template <typename Func>
static constexpr bool is_valid_func_2_v = std::is_invocable_r_v<void, Func, bar>;

// default values
static constexpr auto default_func_1 = [](foo) {};
static constexpr auto default_func_2 = [](bar) {};

// accepting callable 1
template <typename Func1, std::enable_if_t<is_valid_func_1_v<Func1>>* = nullptr>
void my_class(Func1&& func_1)
{
    my_class(std::forward<Func1>(func_1), default_func_2);
}

// accepting callable 1
template <typename Func2, std::enable_if_t<is_valid_func_2_v<Func2>>* = nullptr>
void my_class(Func2&& func_2, bar_tag)
{
    my_class(default_func_1, std::forward<Func2>(func_2));
}

// accepting both
template <
    typename Func1, typename Func2,
    // disallow Func2 to be deduced as bar_tag
    // (not even sure why this check did not work in conjunction with others,
    // even with GCC)
    std::enable_if_t<!std::is_same_v<Func2, bar_tag>>* = nullptr,
    std::enable_if_t<is_valid_func_1_v<Func1> &&
                     is_valid_func_2_v<Func2>>* = nullptr>
void my_class(Func1&& func_1, Func2&& func_2)
{
    std::forward<Func1>(func_1)(foo());
    std::forward<Func2>(func_2)(bar());
}

int main()
{
    my_class([](auto foo) { foo.func1(); });
    my_class([](auto bar) { bar.func2(); }, bar_tag());
}

For Clang, this will result in:

error: no member named 'func1' in 'bar'
my_class([](auto foo) { foo.func1(); });
                        ~~~ ^
...
note: in instantiation of variable template specialization
'is_valid_func_2_v<(lambda at prog.cc:41:14)>' requested here
template <typename Func2, std::enable_if_t<is_valid_func_2_v<Func2>>* = nullptr>
                                           ^

What happened here? Substitution failure is an error?

Edit: It was completely ignorant from me to think that an error inside a predicate of std::enable_if will be silenced as well... That is not a substitution failure.

Fix:

If I put the SFINAE as a function parameter, Clang handles it well. I do not know why deferring the check from the template argument deduction stage to the overload resolution stage makes the difference.

template <typename Func2>
void my_class(Func2&& func_2, bar_tag,
              std::enable_if_t<is_valid_func_2_v<Func2>>* = nullptr)
{
    my_class(default_func_1, std::forward<Func2>(func_2));
}

All in all, I have dived into genericity probably more than I should have with my knowledge and now I am paying for it. So what is it that I am missing? Attentive reader might notice some side questions popping up, but I do not want an answer for all of them. Lastly, I am sorry if a much simpler MCVE could have been made.

xskxzr
  • 12,442
  • 12
  • 37
  • 77
LogicStuff
  • 19,397
  • 6
  • 54
  • 74
  • 1
    Does it work if you call it with `my_class([](foo f) { f.func1(); })`? I can't tell from the cppreference description what the expected behaviour is of `is_invocable_r` in your existing case. In fact, I'd have expected the compile error either way. – John Ilacqua Oct 27 '18 at 11:15
  • @JohnIlacqua [Yes](https://wandbox.org/permlink/njwjR30spUV040c0), with both compilers, and even for `bar`, if we make `bar_tag` optional. – LogicStuff Oct 27 '18 at 11:20
  • 2
    With g++ head 9.0.0 2018 (tried in Wandbox) I get the same error as clang++; not an expert but I suppose that clang++ and more recent g++ are correct. – max66 Oct 27 '18 at 11:27
  • 2
    I think the way you're using `is_invokable_r` isn't quite correct SFINAE. See this: https://godbolt.org/z/cYtb8p. I think the only reason any compiler would accept it is if it's not actually instantiating the `is_valid_func_2` on your `foo` lambda. – John Ilacqua Oct 27 '18 at 11:30
  • 1
    Interesting question. [This](https://gcc.godbolt.org/z/NlncKp) is a simpler program to (possibly) reproduce your problem. – xskxzr Oct 27 '18 at 17:47
  • 1
    Anyway, I asked a [new question](https://stackoverflow.com/q/53024903/5376789). – xskxzr Oct 27 '18 at 18:13

1 Answers1

2

From my understanding, you're not quite using SFINAE correctly - if you ever try to call std::is_invocable_r_v<void, Func, bar>; with Func == decltype([](auto foo) { foo.func1(); } you will get a compiler error, as the auto in the lambda is deduced to bar and then tries to call func1() on it. If your lambda didn't use auto and instead had the actual type as the parameter (i.e. foo, so you can't invoke it with a bar), is_invocable_r_v would return false and the SFINAE would work.

John Ilacqua
  • 906
  • 5
  • 19
  • As a side note, my guess as to why gcc is compiling it is that it's probably ignoring the overload due to having a different number of parameters before it even looks at the `enable_if`s. – John Ilacqua Oct 27 '18 at 11:37
  • Yes, this is the core of the problem. If you could find a standard paragraph allowing compilers unspecified behavior concerning ruling out the overloads (if one exists), that would be the perfect answer. – LogicStuff Oct 27 '18 at 11:39
  • 1
    I unfortunately don't have a copy of the standard to consult (and it's also almost 11pm), so I'm not able to find and add a reference at the moment. I don't even know if it is allowed by the standard - someone else mentioned that it doesn't compile with gcc-9, so perhaps it was wrong and they fixed it. – John Ilacqua Oct 27 '18 at 11:45
  • Okay, thank you very much, and excuse me keeping the question open for a while to attract more readers and their thoughts. – LogicStuff Oct 27 '18 at 11:49