通用Lambda,重载,std :: is_invocable和SFINAE-GCC和Clang的行为不同

时间:2018-10-27 10:45:56

标签: c++ templates language-lawyer c++17 sfinae

问题

我写了一段复杂的模板代码,可以with GCC 8.2.1编译,但是with Clang 7.0不能编译(代码和错误链接)。

我认为这可能是this Q&A的含义,但我看不到。

动机

我正在编写一个类,我希望可以使用两个不同类型的可调用对象构造该类,但也可以省略其中一个,即:

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

那应该没有问题。但是,为什么不允许callable_1callable_2成为函数模板(或带有operator()模板的函子)。也就是说,我想要这个(或至少最初想要的):

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

正如您所看到的,不幸的是,两个可调用对象都具有相同的签名,因此我们需要以某种方式消除它们之间的歧义。我能想到的第一种方法(也是该问题所针对的一种方法)是在一个一元重载中添加一个“ tag”参数:

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

在我看来,这是可以接受的,但是我想出了更好的解决方案:

  • 在第二个可调用对象的签名(最后一个参数或返回类型)中使用标签(如果不明确,则为可选)
  • 使第二个构造函数重载为另一个命名的非成员或static成员函数

我仍然想知道,为什么两种编译器的行为与我最初的方法有所不同,哪一种是正确的(或两者都是正确的)。


代码:

为简单起见,我已将构造函数重载转换为常规的my_class函数重载。

#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());
}

对于Clang,这将导致:

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>
                                           ^

这里发生了什么? 替换失败是错误?

编辑:我完全不知道std::enable_if谓词中的错误也将被忽略...这是不是替换失败。

修复:

如果我将SFINAE作为函数参数,Clang会很好地处理它。 我不知道为什么将检查从模板参数推导阶段推迟到重载解决阶段会有所不同。

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));
}

总而言之,我投入到通用性方面的知识可能比我所了解的要多,现在我要为此付出代价。那我想念的是什么呢?细心的读者可能会注意到一些附带的问题,但是我不希望为所有问题提供答案。最后,很抱歉,如果可以制作出更简单的MCVE。

1 个答案:

答案 0 :(得分:2)

据我所知,您不太正确地使用SFINAE-如果您尝试使用std::is_invocable_r_v<void, Func, bar>;调用Func == decltype([](auto foo) { foo.func1(); },则会出现编译器错误,因为lambda中的auto被推导为{ {1}},然后尝试在其上调用bar。如果您的lambda不使用auto而是使用实际类型作为参数(即func1(),因此您不能使用foo来调用它),则bar将返回false,并且SFINAE将起作用。