[trick] SFINAE with decltype
От: flаt  
Дата: 18.08.14 12:01
Оценка: 149 (8)
Несколько раз натыкался на интересную возможность decltype(..., true) и, в конце концов, решил поискать больше информации.

SO:
// Culled by SFINAE if reserve does not exist or is not accessible
template <typename T>
constexpr auto has_reserve_method(T& t) -> decltype(t.reserve(0), bool()) {
  return true;
}

// Used as fallback when SFINAE culls the template method
constexpr bool has_reserve_method(...) { return false; }


Note: thanks to the semantics of , (the comma operator) you can chain multiple expressions in decltype and only the last actually decides the type. Handy to check multiple operations.


Также нашёл, что и в классических реализациях с sizeof можно делать так же: http://rsdn.ru/forum/cpp/2759773.1
Re: [trick] SFINAE with decltype
От: Кодт Россия  
Дата: 25.08.14 09:44
Оценка: 6 (1)
Здравствуйте, flаt, Вы писали:

F>

Note: thanks to the semantics of , (the comma operator) you can chain multiple expressions in decltype and only the last actually decides the type. Handy to check multiple operations.


Пока какой-нибудь негодяй не перегрузит запятую для своего типа.
К счастью, против этого есть контрмера: void. Он всегда апеллирует к определённой компилятором запятой.

http://ideone.com/CNP5dM
#include <iostream>
using namespace std;

struct Foo {};

int foo(int) { return 0; }
Foo foo(double) { return Foo(); }
// no foo(const char*)

struct Bar { Bar(bool) {} };
ostream& operator << (ostream& ost, Bar&& bar) { return ost << "BARR"; }

Bar operator , (Foo&&, bool) { return Bar(true); } // ВНЕЗАПНО!

// требование к then-ветке функции: чтобы была определена функция foo(T)

template<class T> auto test1(T&& t) -> decltype(foo(t), true) { return true; }
auto test1(...) -> bool { return false; }

template<class T> auto test2(T&& t) -> decltype(foo(t), void(0), true) { return true; }
auto test2(...) -> bool { return false; }


int main()
{
  cout << boolalpha << test1(1) << " " << test1(1.) << " " << test1("") << endl; // true BARR false
  cout << boolalpha << test2(1) << " " << test2(1.) << " " << test2("") << endl; // true true false
  return 0;
}



F>Также нашёл, что и в классических реализациях с sizeof можно делать так же: http://rsdn.ru/forum/cpp/2759773.1
Перекуём баги на фичи!
Re[2]: [trick] SFINAE with decltype
От: Constructor  
Дата: 25.08.14 12:00
Оценка: 6 (1)
Здравствуйте, Кодт, Вы писали:

К>К счастью, против этого есть контрмера: void. Он всегда апеллирует к определённой компилятором запятой.


Только вместо

К>decltype(foo(t), void(0), true)


или

decltype(foo(t), void(), true)

вполне достаточно писать

decltype((void)foo(t), true)

или

decltype(static_cast<void>(foo(t)), true)

Несмотря на потенциальную возможность существования у типа decltype(foo(t)) оператора operator void() неявного приведения к void, по стандарту он никогда не должен быть вызван:

[class.conv.fct] 12.3.2/1

A conversion function is never used to convert a (possibly cv-qualified) object to the (possibly cv-qualified) same object type (or a reference to it), to a (possibly cv-qualified) base class of that type (or a reference to it), or to (possibly cv-qualified) void.


И далее в тексте сноски:

These conversions are considered as standard conversions for the purposes of overload resolution (13.3.3.1, 13.3.3.1.4) and therefore initialization (8.5) and explicit casts (5.2.9). A conversion to void does not invoke any conversion function (5.2.9). Even though never directly called to perform a conversion, such conversion functions can be declared and can potentially be reached through a call to a virtual conversion function in a base class.


В обоих цитатах выделение мое.
Re[3]: [trick] SFINAE with decltype
От: Кодт Россия  
Дата: 25.08.14 13:02
Оценка:
Здравствуйте, Constructor, Вы писали:

C>вполне достаточно писать

C>
C>decltype((void)foo(t), true)
C>


Да, про непосредственное приведение к void я что-то не подумал. Так проще.
Перекуём баги на фичи!
Re[2]: [trick] SFINAE with decltype
От: Ku-ku  
Дата: 26.08.14 09:51
Оценка: 40 (2)
Здравствуйте, Кодт, Вы писали:

К>К счастью, против этого есть контрмера: void. Он всегда апеллирует к определённой компилятором запятой.


Как ни странно, в особо специфических случаях перегруженная запятая может сломать код, где операнд запятой имеет тип void

http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1549
Re[3]: [trick] SFINAE with decltype
От: Кодт Россия  
Дата: 27.08.14 16:17
Оценка:
Здравствуйте, Ku-ku, Вы писали:

KK>Как ни странно, в особо специфических случаях перегруженная запятая может сломать код, где операнд запятой имеет тип void

KK>http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1549

Какой-то ужасно хрупкий код. Стоит разнести проверяемый тип (typename T::type::type) и ломающий компиляцию возвращаемый (A<void>) — и компилятор весело забивает на перегрузку void,X

Попробовал сделать комбинаторный взрыв и получить минимальный пример
#include <cstdio>

template<class M> struct Match          { typedef M type; };
template<class V> struct Yield          { typedef V type; V var; };
template<class V, class M> struct Combo { typedef M type; V var; };

template<class Y, class M> struct Arg
{
    typedef typename M::type match;
};


template<class V> struct YM : Arg< Yield<V>,      Match<V>      > {};
template<class V> struct YY : Arg< Yield<V>,      Yield<V>      > {};
template<class V> struct CC : Arg< Combo<V,char>, Combo<char,V> > {};

template<class V> struct YMi : Arg< Yield<int>,      Match<V>      > {};
template<class V> struct YYi : Arg< Yield<int>,      Yield<V>      > {};
template<class V> struct CCi : Arg< Combo<int,char>, Combo<char,V> > {};


template<class T> void operator , (typename T::match, T)
{
    printf("called %s\n", __PRETTY_FUNCTION__);
}

int main()
{
    int(), YM<int>(); // шаблонный
    int(), YY<int>(); // шаблонный
    int(), CC<int>(); // шаблонный

    void(), YM<void>(); // проверяется шаблонный, выполняется дефолтный
//    void(), YY<void>();
//    void(), CC<void>();

    int(), YMi<int>(); // шаблонный
    int(), YYi<int>(); // шаблонный
    int(), CCi<int>(); // шаблонный

    void(), YMi<void>(); // проверяется шаблонный, выполняется дефолтный
//    void(), YYi<void>();
    void(), CCi<void>(); // проверяется шаблонный, выполняется дефолтный
}

Что характерно, неполный тип — Yield<void> или Combo<???,void> — нигде не используется, кроме как будучи формальным параметром шаблона Arg.
При этом void(),YM<void>() прокатило, а void(),YYi<void>() нет.

Суть перебора: участвует ли неполный тип
— в выводе зависимого типа (валится!)
— в роли другого аргумента шаблона Arg
— — в выводе зависимого типа используется такой же шаблон (валится!!!) — т.е. Yield,Yield или Combo,Combo
— — в выводе зависимого типа используется другой шаблон — т.е. Yield<void>,Match<void> и т.п.
Перекуём баги на фичи!
Re[4]: [trick] SFINAE with decltype
От: Ku-ku  
Дата: 27.08.14 18:23
Оценка:
Tl;dr

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

template <class T>
struct S
{
    typedef T type;
    typedef typename T::type nested_type;
};

void f(int) {}

template <class T>
typename S<T>::type f(T) {}

int main()
{
    f(0);
}

Здесь у int пытаются получить вложенный тип type, которого там конечно же нет. Припрятать такие грабли в запятой довольно легко

template <class T>
struct S
{
    typedef T type;
    typedef typename T::type nested_type;
};

struct A {};

template <class T>
typename S<T*>::type operator,(A, T) {}

int main()
{
    A(), void();
}

Суть та же, что и в предыдущем примере, только здесь void* вместо int.
Re[5]: [trick] SFINAE with decltype
От: Кодт Россия  
Дата: 27.08.14 19:47
Оценка:
Здравствуйте, Ku-ku, Вы писали:

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


Ненене. Вот, например, shared_ptr можно параметризовать неполным типом (предобъявленным), и это не делает программу невалидной; либо имеет место наглая эксплуатация дефекта стандарта?

В моём коде нигде нет обращения к тем свойствам неполного типа, которые могли бы что-то сломать. Ни экземпляр его пытаюсь создать, ни даже размер спросить. Ни даже указатель получить!
Только выковыриваю зависимое имя типа, которое там со всей очевидностью присутствует.

Причём компилятор — ну ладно бы ругался на выковыривание зависимого типа из неполного типа; так нет же, он ругается тогда, когда зависимый тип выковыривается из полноценного типа — полученного из того же шаблона, но с другими параметрами.
Это вообще цирк какой-то.
Перекуём баги на фичи!
Re[4]: [trick] SFINAE with decltype
От: Constructor  
Дата: 28.08.14 07:15
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Попробовал сделать комбинаторный взрыв и получить минимальный пример


Странно, в Вашем примере строчка void(), CC<void>(); компилируется:

clang 3.4.2
g++ 4.9.0
vc++ 2013
Re[6]: [trick] SFINAE with decltype
От: Ku-ku  
Дата: 28.08.14 08:32
Оценка:
Здравствуйте, Кодт, Вы писали:

К>В моём коде нигде нет обращения к тем свойствам неполного типа, которые могли бы что-то сломать.


Речь про эти закомментированные строчки?

void(), YY<void>();

void(), YYi<void>();

Там у Yield<void> запрашивают вложенный тип type, что приводит к необходимости инстанцировать определение Yield<void>, которое невалидно из-за наличия в нём поля типа void.
Re[5]: [trick] SFINAE with decltype
От: Кодт Россия  
Дата: 28.08.14 08:46
Оценка:
Здравствуйте, Constructor, Вы писали:

C>Странно, в Вашем примере строчка void(), CC<void>(); компилируется:


Это я, видимо, в процессе экспериментов что-то оставил. У меня тоже скомпилировалось (gcc 4.7 cygwin)
Перекуём баги на фичи!