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.