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 и других.