Общие лямбды, перегрузка, std :: is_invocable и SFINAE - различное поведение между GCC и Clang - PullRequest
0 голосов
/ 27 октября 2018

Проблема

Я написал сложный фрагмент кода шаблона, который можно скомпилировать с GCC 8.2.1 , но не с Clang 7.0 (код и ссылки на ошибки).

Я думаю, это может быть следствием этих вопросов и ответов , но я не могу его увидеть.

Мотивация

Я пишуКласс, который я хотел бы конструировать с двумя вызываемыми различными типами, но также с одним из них опущенным, то есть:

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

Это должно пройти без проблем.Но почему бы не разрешить callable_1 и callable_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 голосов
/ 27 октября 2018

Насколько я понимаю, вы не совсем правильно используете SFINAE - если вы когда-нибудь попытаетесь вызвать std::is_invocable_r_v<void, Func, bar>; с Func == decltype([](auto foo) { foo.func1(); }, вы получите ошибку компилятора, так как auto в лямбда-выражении выводится на bar, а затем пытается позвонить func1() на это. Если ваша лямбда не использовала auto и вместо этого имела фактический тип в качестве параметра (т. Е. foo, поэтому вы не можете вызвать его с bar), is_invocable_r_v вернет false и SFINAE будет работать.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...