Как сделать «глубокий» SFINAE, т. Е. Когда подстановка вызывает некоторые ошибки компиляции в дальнейшем в коде? - PullRequest
2 голосов
/ 01 июля 2019

Мне нужно реализовать тип обнаружения, например is_invocable, но похоже, что SFINAE выполняет только поверхностную проверку ошибки замещения, тогда как с моим is_invocable мне нужно иметь возможность корректно определить, будет ли этот вызов компилироваться совсем. Можно ли добиться с C ++ 17?

https://godbolt.org/z/Y72Dov

#include <type_traits>

struct supported_by_f1_and_f2 {};
struct not_supported_by_f1 {};
struct not_supported_by_f2 {};

template<typename T>
auto f2(T t, std::enable_if_t<!std::is_same_v<T, not_supported_by_f2>>* = 0) {}

template<typename T>
auto f1(T t, std::enable_if_t<!std::is_same_v<T, not_supported_by_f1>>* = 0) {
    return f2(t);
}

template <typename T, typename = void>
struct is_f1_invocable : public std::false_type {};

template <typename T>
struct is_f1_invocable<T, std::void_t<decltype(f1(std::declval<T>()))>> : public std::true_type {};

using supported_by_f1_and_f2_ok_t = is_f1_invocable<supported_by_f1_and_f2>;
using not_supported_by_f1_ok_t = is_f1_invocable<not_supported_by_f1>;
using not_supported_by_f2_ok_t = is_f1_invocable<not_supported_by_f2>;

supported_by_f1_and_f2_ok_t supported_by_f1_and_f2_ok;
not_supported_by_f1_ok_t not_supported_by_f1_ok;

// Why substitution failure, that occures during 'return f2(t);', is not detected here during the instantiation of 'is_f1_invocable'?
not_supported_by_f2_ok_t not_supported_by_f2_ok; // error: no matching function for call to 'f2'

EDIT:

С https://en.cppreference.com/w/cpp/language/sfinae:

Только ошибки в типах и выражениях в непосредственном контексте типа функции или ее типов параметров шаблона [или ее явного спецификатора (начиная с C ++ 20)] являются ошибками SFINAE. Если оценка замещенного типа / выражения вызывает побочный эффект, такой как создание некоторой специализации шаблона, генерирование неявно определенной функции-члена и т. Д., Ошибки в этих побочных эффектах рассматриваются как серьезные ошибки. [Лямбда-выражение не считается частью непосредственного контекста. (начиная с C ++ 20)]

Так есть ли способ расширить / обойти это?

Ответы [ 2 ]

2 голосов
/ 02 июля 2019

Концепция, которую вы ищете, заключается в том, чтобы f1 был SFINAE дружественным .Это требует, чтобы author из f1 предпринял некоторые действия, чтобы гарантировать, что user имеет какой-то способ обнаружить, что вызов f1 будет плохо сформирован, что приведет кмягкая ошибка.Если f1 написано не для дружественного SFINAE, то обходного пути нет.

Чтобы сделать дружественным f1 SFINAE, мы должны убедиться, что перед некоторой ошибкой компиляции, которая возникнет при создании экземпляра тело из f1 достигается, во-первых, условие, которое вызвало бы эту ошибку, делает подпись из f1 недействительной, так что, когда окружающая инстанциация пытается вызвать или принять адрес f1, SFINAE включается, чтобы удалить f1 из набора перегрузки, поскольку ошибка возникла в непосредственном контексте создания экземпляра подписи f1.

Другими словами, в этом случае, поскольку мы считаем, чтосоздание экземпляра вызова f2(t) в теле f1 может вызвать ошибку, мы должны продублировать этот вызов в подписи f1.Например, мы могли бы сделать это следующим образом:

template <typename T>
auto f1(T t, std::enable_if_t<...>* = 0) -> decltype(f2(t)) {  // actually you may want to decay the type but w/e
    return f2(t);
}

Итак, создание экземпляра f1(std::declval<T>()) запускает процесс замены и вычета для f1, который запускает процесс замены и вычета для f2.В этот момент, благодаря enable_if, происходит ошибка замены в сигнатуре f2, что находится в непосредственном контексте f2 создания экземпляра, поэтому шаблон f2 удаляется из набора перегрузки.В результате вызов f2 в сигнатуре f1 должен быть разрешен из пустого набора перегрузки, что означает, что сбой разрешения перегрузки находится в непосредственном контексте экземпляра f1.Наконец, это также удаляет шаблон f1 из набора перегрузки, что снова приводит к сбою разрешения перегрузки из-за пустого набора перегрузки, на этот раз в непосредственном контексте реализации is_f1_invocable, что нам и нужно.

Аналогичным образом, если что-то может пойти не так при создании экземпляра тела f2, тогда нам нужно изменить сигнатуру f2, чтобы учесть эту возможность, а также обеспечить распространение SFINAE аналогичным образом.

Конечно, вы должны решить, как далеко вы хотите это сделать.В какой-то момент вы можете решить, что вы действительно хотите вызвать серьезную ошибку на этом этапе, вместо того, чтобы просто удалить сигнатуру из набора перегрузки, передав мягкую ошибку в прилагаемую реализацию.

1 голос
/ 02 июля 2019

Нет, это невозможно, именно из-за правила "непосредственного контекста" в [temp.fct.spec] / 8 и описано по вашей ссылке cppreference.com.

Конечно, если f1 проверяется на not_supported_by_f2 в его проверке enable_if_t, либо напрямую, либо путем проверки, можно ли вызвать f2(t), тогда это будет "более SFINAE-правильным", и это не будет проблемой.Но если вы не можете изменить объявление f1, все, что вы можете сделать:

  • добавить дополнительные проверки к вашим чертам, чтобы обойти определенные известные сбои (хотя, если f1находится в библиотеке, которая не находится под вашим контролем, и ее реализация изменится в более поздней версии ...)

    template <typename T>
    struct is_f1_invocable<T,
        std::void_t<decltype(f1(std::declval<T>())),
                    decltype(f2(std::declval<T>()))>> // hack
      : public std::true_type {};
    
  • задокументируйте ограничение, чтобы предупредить программистов, использующих эту черту.

...