Void_t и trailing возвращаемый тип с decltype: они полностью взаимозаменяемы?

Рассмотрим следующий базовый пример, основанный на void_t:

template<typename, typename = void_t<>>
struct S: std::false_type {};

template<typename T>
struct S<T, void_t<decltype(std::declval<T>().foo())>>: std::true_type {};

Его можно использовать, как следует:

template<typename T>
std::enable_if_t<S<T>::value> func() { }

То же самое можно сделать с использованием возвращаемого возвращаемого типа и decltype:

template<typename T>
auto func() -> decltype(std::declval<T>().foo(), void()) { }

Это верно для всех примеров, о которых я думал. Мне не удалось найти случай, в котором может использоваться либо void_t, либо возвращаемый тип возврата с помощью decltype, в то время как его аналог не может. Наиболее сложные случаи могут быть решены с помощью комбинации возвращаемого типа возврата и перегрузки (в качестве примера, когда детектор используется для переключения между двумя функциями вместо триггера для отключения или включения чего-либо).

Это так? Являются ли они (void_t и decltype в качестве возвращаемого типа возврата плюс перегрузка при необходимости) полностью взаимозаменяемы?
В противном случае, какой случай, в котором нельзя использовать для ограничения ограничений, и я вынужден использовать определенный метод?

Ответы

Ответ 1

Это эквивалент метапрограммирования: следует ли написать функцию или просто написать свой код в строке. Причины предпочитать писать черту типа такие же, как и причины предпочитать писать функцию: она более самодокументируется, она многократно используется, ее легче отлаживать. Причины предпочитать писать trailing decltype аналогичны причинам, которые предпочитают писать код inline: это одноразовый, который нельзя использовать повторно, поэтому зачем вкладывать в него усилия, разлагая его и придумывая разумное имя для него

Но вот несколько причин, по которым вам может понадобиться черта типа:

Повторения

Предположим, у меня есть черта, которую я хочу проверять много раз. Как fooable. Если однажды я напишу черту типа, я могу рассматривать это как концепцию:

template <class, class = void>
struct fooable : std::false_type {};

template <class T>
struct fooable<T, void_t<decltype(std::declval<T>().foo())>>
: std::true_type {};

И теперь я могу использовать ту же концепцию в тоннах мест:

template <class T, std::enable_if_t<fooable<T>{}>* = nullptr>
void bar(T ) { ... }    

template <class T, std::enable_if_t<fooable<T>{}>* = nullptr>
void quux(T ) { ... }

Для понятий, которые проверяют больше одного выражения, вы не хотите повторять его каждый раз.

компонуемости

Идя вместе с повторением, составление двух разных типов символов легко:

template <class T>
using fooable_and_barable = std::conjunction<fooable<T>, barable<T>>;

Сопоставление двух возвращаемых типов возврата требует выписывания всех выражений...

Отрицание

С типом признака легко проверить, что тип не удовлетворяет признаку. Это просто !fooable<T>::value. Вы не можете написать выражение trailing- decltype для проверки того, что что-то недействительно. Это может произойти, если у вас есть две непересекающиеся перегрузки:

template <class T, std::enable_if_t<fooable<T>::value>* = nullptr>
void bar(T ) { ... }

template <class T, std::enable_if_t<!fooable<T>::value>* = nullptr>
void bar(T ) { ... }

который хорошо вписывается в...

Отправка тегов

Предполагая, что у нас есть короткая характеристика типа, гораздо яснее отмечать отправку с типом типа:

template <class T> void bar(T , std::true_type fooable) { ... }
template <class T> void bar(T , std::false_type not_fooable) { ... }
template <class T> void bar(T v) { bar(v, fooable<T>{}); }

чем это было бы иначе:

template <class T> auto bar(T v, int ) -> decltype(v.foo(), void()) { ... }
template <class T> void bar(T v, ... ) { ... }
template <class T> void bar(T v) { bar(v, 0); }

0 и int/... немного странно, правильно?

static_assert

Что делать, если я не хочу использовать SFINAE для концепции, а просто хочу просто потерпеть неудачу с ясным сообщением?

template <class T>
struct requires_fooability {
    static_assert(fooable<T>{}, "T must be fooable!");
};

Концепция

Когда (если?) мы когда-либо получаем понятия, очевидно, что использование понятий гораздо более эффективно, когда речь идет обо всем, что связано с метапрограммированием:

template <fooable T> void bar(T ) { ... }

Ответ 2

Я использовал как void_t, так и trailing decltype, когда я реализовал свою собственную домашнюю версию Concepts Lite (кстати, я преуспел), что потребовало создания многих дополнительных типов признаков, большинство из которых используют тактику детектирования так или иначе, Я использовал void_t, trailing decltype и before decltype.

Насколько я понял, эти параметры логически эквивалентны, поэтому идеальный 100% -конформирующий компилятор должен давать одинаковый результат, используя все из них. Проблема, однако, в том, что конкретный компилятор может (и будет) следовать различным шаблонам создания экземпляров в разных случаях, и некоторые из этих шаблонов могут выйти за пределы внутренних ограничений компилятора. Например, когда я попытался сделать MSVC 2015 Update 2 3 обнаружение наличия умножения на тот же тип, единственным работающим решением было предшествующее decltype:

    template<typename T>
    struct has_multiplication
    {
        static no_value test_mul(...);

        template<typename U>
        static decltype(*(U*)(0) *= std::declval<U>() * std::declval<U>()) test_mul(const U&);

        static constexpr bool value = !std::is_same<no_value, decltype(test_mul(std::declval<T>())) >::value;
    };

В каждой другой версии появились внутренние ошибки компилятора, хотя некоторые из них отлично работали с Clang и GCC. Мне также пришлось использовать *(U*)(0) вместо declval, потому что использование трех declval в строке, хотя и совершенно законно, было просто для компилятора в данном конкретном случае.

Плохо, я забыл. Фактически я использовал *(U*)(0), потому что declval создает rvalue-ref типа, которому нельзя назначить, и почему я использовал это. Но все остальное остается в силе, эта версия работает там, где другие не сделали.

Итак, теперь мой ответ будет следующим: "они идентичны, если ваш компилятор думает, что они есть". И это то, что вам нужно выяснить при тестировании. Я надеюсь, что это перестанет быть проблемой в следующих выпусках MSVC и других.