[GCC/C++11] Bug?
От: zaufi Земля  
Дата: 02.03.13 13:32
Оценка: 14 (1)
hi all

потребовался мне тут чекер компаил-таймный чтобы проверять можно ли некий callable-type вызвать с заданными параметрами.
собственно сам чекер, дело не хитрое, но пепец подкрался откуда не ждал: в тесте он попросту не работал ;( я некоторое (заметное) время я еще пробовал его реанимировать и так и эдак печатая разнообразный debug spam про типы тут участвующие -- все казалось бы хорошо, но финальный чекер не работает хоть тресни ;(

затрахавшись с ним в конец (и уже собираясь сдаваться) я попробовал собрать тестик clang'ом 3.2 -- и он зараза работает как и ожидалось!
я уже было сильно расстроился и собрался писать багу в GCCшную багзилу, сделал минимальный пример (см ниже), но все таки решил слегка его изменить (см вариант #1 и #2 в примере) -- оказалось что мой изначальный вариант (без add_pointer) ни в какую не хочет работать (что очевидно бага), но второй работает замечательно (что меня все таки радует)...

// kate: hl C++11;
#include <iostream>
#include <type_traits>
#include <utility>

struct is_callable_with_checker
{
    typedef char yes_type;
    typedef char (&no_type)[2];

    template <typename...>
    static no_type test(...);

#if 1
    // case #1 (original): simple add '*' to a deduced type to form a pointer type parameter
    template <typename Callable, typename... Args>
    static yes_type test(
        /*const*/ decltype(
            std::declval<Callable>()(std::forward<Args>(std::declval<Args>())...)
          , void()
          )* /*const*/
      );
#else
    // case #2: form a pointer type via metafunction
    template <typename Callable, typename... Args>
    static yes_type test(
        typename std::add_pointer<
            decltype(
                std::declval<Callable>()(std::forward<Args>(std::declval<Args>())...)
              , void()
              )
          >::type
      );
#endif
};

template <typename Callable, typename... Args>
struct is_callable_with
  : std::is_same<
        decltype(is_callable_with_checker::template test<Callable, Args...>(nullptr))
      , is_callable_with_checker::yes_type
      >
{};

// Some functor w/ overloaded operator(...)
struct test
{
    int operator()(int) const
    {
        return 123;
    }
    double operator()(int, double) const
    {
        return 123.123;
    }
};

int main()
{
    // expect 1
    std::cout << is_callable_with<test, int>::type::value << std::endl;
    // expect 1
    std::cout << is_callable_with<test, int, double>::type::value << std::endl;
    // expect 0
    std::cout << is_callable_with<test, int, std::ostream&>::type::value << std::endl;

    return 0;
}


собственно вопросы к просвещенной общественности:
0) кто видит разницу в получаемом типе параметра функции `yes_type test(...)`?
1) если ее нету, очевидно это бага -- может кто знает с чем (с какой другой багой) она связана? может уже зарепортили что-то похожее?

PS: кастую Masterkent'а
Re: [GCC/C++11] Bug?
От: andyp  
Дата: 03.03.13 15:17
Оценка:
Может не понял что:
При инстанциировании шаблонных функций из is_callable_with_checker дя test имеем следующие перегрузки для функции test:

1. ifdef 1
no_type test(...);
yes_type test(int,void());

Для аргумента-указателя nullptr компилятор более подходящей считает видимо первую (не знаю, по стандарту ли это). Магию с void() не понял совсем.
2. вторая века ifdef (где add_pointer)

no_type test(...);
yes_type test(int*,void());

Компилятор выбирает вторую перегрузку т.к. она имеет аргумент-указатель, что в общем-то очень естественно.

Как-то так.
Re[2]: [GCC/C++11] Bug?
От: andyp  
Дата: 03.03.13 15:33
Оценка:
Разобрался

Кандидаты выглядят как-то так:
1. ifdef 1
no_type test(...);
yes_type test(void);

2. вторая века ifdef (где add_pointer)

no_type test(...);
yes_type test(void*);

Компилятор выбирает вторую перегрузку т.к. она имеет аргумент-указатель, что в общем-то очень естественно.
Re[3]: [GCC/C++11] Bug?
От: andyp  
Дата: 03.03.13 16:06
Оценка:
pps там еще decltype ссылку накручивает — вот так тоже работает

struct is_callable_with_checker
{
    typedef void* vp;
    typedef char yes_type;
    typedef char (&no_type)[2];

    template <typename...>
    static no_type test(...);

#if 1
    // case #1 (original): simple add '*' to a deduced type to form a pointer type parameter
    template <typename Callable, typename... Args>
    static yes_type test(
       typename std::remove_reference<
        /*const*/ decltype(
            std::declval<Callable>()(std::forward<Args>(std::declval<Args>())...)
          , vp()
          )* /*const*/
      >::type          
      );
#else
    // case #2: form a pointer type via metafunction
    template <typename Callable, typename... Args>
    static yes_type test(
        typename std::add_pointer<
            decltype(
                std::declval<Callable>()(std::forward<Args>(std::declval<Args>())...)
              ,cp()
              )
          >::type
      );
#endif
};
Re[2]: [GCC/C++11] Bug?
От: zaufi Земля  
Дата: 03.03.13 16:57
Оценка:
Здравствуйте, andyp, Вы писали:

A>Магию с void() не понял совсем.


"магия" состоит вот в чем:
выражение 'decltype(Expression)*' означает вот что: к типу получаемому внутри decltype, прицепляется '*', давая в результате указатель на тип. 'Expression' это unevaluated operand служит лишь для того, чтобы получить тип являющийся результатом выражения и состоит он, в свою очередь из 'TestExpression, void()' -- т.е. это operator,() результатом которого всегда является `void` (что в итоге должно дать void*), а TestExpression всего лишь проверяется на валидность (и тут работает SFINAE -- не правильные выражения просто сделают невалидной перегрузку `yes_type test(...)`. таким образом должен получиться следующий набор перегрузок:
no_type test(...);
yes_type test(void*);


это если все хорошо (в таком случае `test(nullptr)` будет предпочтительней эллипса) и только первая перегрузка, если тестируемое выражение все таки не валидно...

вот тестик, чтобы "поиграться":

// kate: hl C++11;
#include <iostream>
#if defined(__GNUC__)
#  include <cxxabi.h>
#  include <cassert>
#  include <cstdlib>
#  include <memory>
#endif
#include <string>
#include <typeinfo>

inline std::string demangled_name(const char* const name)
{
    // Try to demangle a given name using gcc API (of couse if current compiler is gcc/clang)
# if defined(__GNUC__)
    int status;
    std::unique_ptr<char, decltype(&std::free)> demangled_name = {
        abi::__cxa_demangle(name, 0, 0, &status)
      , &std::free
      };
    if (!status)
        return demangled_name.get();
    assert("How is that possible? Fail on demangle name produced by itself??" && false);
# endif                                                     // __GNUC__
    return name;
}

template <typename T>
inline std::string type_name()
{
    return demangled_name(typeid(T).name());
}

int foo()
{
    return 123;
}
double foo(int)
{
    return 123.123;
}

int main()
{
    std::cout << type_name<decltype(foo(), void())*>() << std::endl;
    std::cout << type_name<decltype(foo(123), void())*>() << std::endl;
    return 0;
}


zaufi@gentop /work/tests $ g++11 -o decltype_test decltype_test.cc
zaufi@gentop /work/tests $ ./decltype_test
void*
void*


как видишь результат operator,() внутри decltype всегда void.
Re[4]: [GCC/C++11] Bug?
От: zaufi Земля  
Дата: 03.03.13 17:13
Оценка:
Здравствуйте, andyp, Вы писали:

A>pps там еще decltype ссылку накручивает — вот так тоже работает


что-то я в упор не вижу откуда там может взяться ссылка... ;( — да и тест (см мой другой ответ тебе) ее не видит...
похоже это "работает" все тем же "магическим" образом что и с add_pointer -- выглядит какбудто использование stdшных type traitsов что-то "переключает" внутри глючного gcc... в clang же все работает без этих грязных фокусов...
Re[5]: [GCC/C++11] Bug?
От: andyp  
Дата: 03.03.13 17:47
Оценка:
Здравствуйте, zaufi, Вы писали:

Z>Здравствуйте, andyp, Вы писали:


A>>pps там еще decltype ссылку накручивает — вот так тоже работает


Z>что-то я в упор не вижу откуда там может взяться ссылка... ;( — да и тест (см мой другой ответ тебе) ее не видит...

Z>похоже это "работает" все тем же "магическим" образом что и с add_pointer -- выглядит какбудто использование stdшных type traitsов что-то "переключает" внутри глючного gcc... в clang же все работает без этих грязных фокусов...

со ссылкой как раз понятно — 7.1.6.2 (4) у меня n3376:

The type denoted by decltype(e) is defined as follows:
— if e is an unparenthesized id-expression or an unparenthesized class member access (5.2.5), decltype(e)
is the type of the entity named by e. If there is no such entity, or if e names a set of overloaded functions,
the program is ill-formed;
— otherwise, if e is an xvalue, decltype(e) is T&&, where T is the type of e;
— otherwise, if e is an lvalue, decltype(e) is T&, where T is the type of e;
— otherwise, decltype(e) is the type of e.

у тебя на мой взгляд void() (ну или vp() у меня )- это lvalue, поэтому decltype довешивает ссылку
Re[3]: [GCC/C++11] Bug?
От: andyp  
Дата: 03.03.13 17:49
Оценка:
У меня с operator, проблемы возникли и скобочки не так подсчитал. Потом осенило как это работает — decltype подсовывается тип правого выражения после запятой.
Re: [GCC/C++11] Bug?
От: rg45 СССР  
Дата: 03.03.13 20:13
Оценка:
Здравствуйте, zaufi, Вы писали:

Z>hi all


Z>потребовался мне тут чекер компаил-таймный чтобы проверять можно ли некий callable-type вызвать с заданными параметрами.

Z>собственно сам чекер, дело не хитрое, но пепец подкрался откуда не ждал: в тесте он попросту не работал ;( я некоторое (заметное) время я еще пробовал его реанимировать и так и эдак печатая разнообразный debug spam про типы тут участвующие -- все казалось бы хорошо, но финальный чекер не работает хоть тресни ;(

Z>затрахавшись с ним в конец (и уже собираясь сдаваться) я попробовал собрать тестик clang'ом 3.2 -- и он зараза работает как и ожидалось!

Z>я уже было сильно расстроился и собрался писать багу в GCCшную багзилу, сделал минимальный пример (см ниже), но все таки решил слегка его изменить (см вариант #1 и #2 в примере) -- оказалось что мой изначальный вариант (без add_pointer) ни в какую не хочет работать (что очевидно бага), но второй работает замечательно (что меня все таки радует)...

Z>
  Пример
Z>// kate: hl C++11;
Z>#include <iostream>
Z>#include <type_traits>
Z>#include <utility>

Z>struct is_callable_with_checker
Z>{
Z>    typedef char yes_type;
Z>    typedef char (&no_type)[2];

Z>    template <typename...>
Z>    static no_type test(...);

Z>#if 1
Z>    // case #1 (original): simple add '*' to a deduced type to form a pointer type parameter
Z>    template <typename Callable, typename... Args>
Z>    static yes_type test(
Z>        /*const*/ decltype(
Z>            std::declval<Callable>()(std::forward<Args>(std::declval<Args>())...)
Z>          , void()
Z>          )* /*const*/
Z>      );
Z>#else
Z>    // case #2: form a pointer type via metafunction
Z>    template <typename Callable, typename... Args>
Z>    static yes_type test(
Z>        typename std::add_pointer<
Z>            decltype(
Z>                std::declval<Callable>()(std::forward<Args>(std::declval<Args>())...)
Z>              , void()
Z>              )
          >>::type
Z>      );
Z>#endif
Z>};

Z>template <typename Callable, typename... Args>
Z>struct is_callable_with
Z>  : std::is_same<
Z>        decltype(is_callable_with_checker::template test<Callable, Args...>(nullptr))
Z>      , is_callable_with_checker::yes_type
      >>
Z>{};

Z>// Some functor w/ overloaded operator(...)
Z>struct test
Z>{
Z>    int operator()(int) const
Z>    {
Z>        return 123;
Z>    }
Z>    double operator()(int, double) const
Z>    {
Z>        return 123.123;
Z>    }
Z>};

Z>int main()
Z>{
Z>    // expect 1
Z>    std::cout << is_callable_with<test, int>::type::value << std::endl;
Z>    // expect 1
Z>    std::cout << is_callable_with<test, int, double>::type::value << std::endl;
Z>    // expect 0
Z>    std::cout << is_callable_with<test, int, std::ostream&>::type::value << std::endl;

Z>    return 0;
Z>}
Z>


Z>собственно вопросы к просвещенной общественности:

Z>0) кто видит разницу в получаемом типе параметра функции `yes_type test(...)`?
Z>1) если ее нету, очевидно это бага -- может кто знает с чем (с какой другой багой) она связана? может уже зарепортили что-то похожее?

Очевидно, бага. Минимизированный воспроизводящий пример:
template<typename...> 
struct Foo { };

template<typename... Args>
void foo(Foo<Args...>*) { }

int main()
{
   foo<int>(nullptr); //error: no matching function for call to 'foo'
}


Кстати, clang это тоже не переваривает.

Ну и так, мимоходом, std::forward в твоем случае никакой роли не играют, их можно выкинуть.

Ну и кроме того, пора начинать уже юзать шаблонные using declaration, читабельность кода от этого будет выигрывать:
template<typename T>
using AddPointer = typename std::add_pointer<T>::type;

yes_type test(AddPointer<decltype(std::declval<Callable>()(std::declval<Args>())...));
--
Не можешь достичь желаемого — пожелай достигнутого.
Re[2]: [GCC/C++11] Bug?
От: Кодт Россия  
Дата: 03.03.13 20:25
Оценка: 7 (1)
Здравствуйте, rg45, Вы писали:

R>Очевидно, бага. Минимизированный воспроизводящий пример:

R>
R>template<typename...> 
R>struct Foo { };

R>template<typename... Args>
R>void foo(Foo<Args...>*) { }

R>int main()
R>{
R>   foo<int>(nullptr); //error: no matching function for call to 'foo'
R>}
R>


Простите, а в чём здесь баг компилятора? Как из nullptr можно вывести параметры Arg... шаблона Foo?
Перекуём баги на фичи!
Re[3]: [GCC/C++11] Bug?
От: rg45 СССР  
Дата: 03.03.13 20:28
Оценка: +1
Здравствуйте, Кодт, Вы писали:

К>Здравствуйте, rg45, Вы писали:


R>>Очевидно, бага. Минимизированный воспроизводящий пример:

R>>
R>>template<typename...> 
R>>struct Foo { };

R>>template<typename... Args>
R>>void foo(Foo<Args...>*) { }

R>>int main()
R>>{
R>>   foo<int>(nullptr); //error: no matching function for call to 'foo'
R>>}
R>>


К>Простите, а в чём здесь баг компилятора? Как из nullptr можно вывести параметры Arg... шаблона Foo?


А зачем их выводить, когда они указаны явно?
--
Не можешь достичь желаемого — пожелай достигнутого.
Re: До меня дошло! :)
От: rg45 СССР  
Дата: 05.03.13 09:33
Оценка: 105 (4)
Здравствуйте, zaufi, Вы писали:

Z>собственно вопросы к просвещенной общественности:

Z>0) кто видит разницу в получаемом типе параметра функции `yes_type test(...)`?

Долго шло, но дошло все-таки, и это радует

Начнем по порядку. Для начала разберемся, почему не компилируется пример, приведенный здесь
Автор: rg45
Дата: 04.03.13
. Еще раз приведу его:

template<typename...> 
struct Foo { };

template<typename... Args>
void foo(Foo<Args...>*) { }

int main()
{
   foo<int>(nullptr); //error: no matching function for call to 'foo'
}

В данном случае, несущественно, что вместо decltype используется просто шаблон класса с переменный числом параметров. Также понятно, что в оригинальном примере zaufi ошибка компиляции не возникает в силу принципа SFINAE — Substitution Failure Is Not An Error.

Причина такого поведения кроется в том, что для шаблонных функций работает (должно работать по стандарту) частичное выведение шаблонных параметров — при вызове не обязательно указывать полный список шаблонных параметров, достаточно указать только несколько первых параметров, оставшиеся будут выведены из фактических параметров функции:
template<typename...> 
struct Foo { };

template<typename... Args>
void foo(Foo<Args...>*) { }

int main()
{
   Foo<int, char, double> t;
   foo<int>(&t);
}


Обратите внимание, что список шаблонных параметров, указанных при вызове функции лишь частично совпадает со списком фактических параметров типа аргумента. Этот пример успешно компилируют и clang и gcc. Т.о. в случае, когда шаблон функции имеет переменное число параметров, причем эти параметры могут выводиться из фактических параметров функции, возникает конфликт — число параметров, определенных одним и другим способом может оказаться разным. Не удивительно, что компилятор при разрешении этого конфликта отдает предпочтение выведению шаблонных параметров функции по ее фактическим параметрам, потому что этот способ позволяет вывести полный список шаблонных параметров.

Идем дальше. Как только мы используем add_pointer для объявления формального параметра функции:
template<typename... Args>
void foo(typename std::add_pointer<Foo<Args...>>::type) { }

мы тем самым вводим невозможность выведения типа шаблонных параметров из фактических, поскольку определение типа, используемого для формального параметра, оказывается вложенным в класс, и компилятор не может "решить уравнение типов" и вывести внешний тип из внутреннего. Этот вопрос не один раз уже обсуждался на этом форуме, например, здесь
Автор: rg45
Дата: 24.03.06
и выше по ветке обсуждения. Ну и как только выведение шаблонных параметров перестает работать, компилятор, естественно, начинает использовать параметры, указанные явно, что и является желательным для нашего случая.

Т.о. можно сказать, что gcc себя проявил более строго и последовательно в этом случае по сравнению с clang.
--
Не можешь достичь желаемого — пожелай достигнутого.
Re: Заставляем пример работать
От: rg45 СССР  
Дата: 05.03.13 10:51
Оценка: 70 (2)
Здравствуйте, zaufi, Вы писали:

<>

Раз уж мы разобрались в причинах такого поведения здесь
Автор: rg45
Дата: 05.03.13
, возникает естественный вопрос, как можно заставить этот пример работать (помимо использования вложенности, как в случае с использованием add_pointer). Оказывается, еще одним способом заставить компилятор использовать список явно указанных параметров вместо их выведения — это сделать nullptr значением по умолчанию:
template<typename...> 
struct Foo { };

template<typename... Args>
void foo(Foo<Args...>* = nullptr) { }

int main()
{
   foo<int>(); // Ok!
   foo<int>(nullptr); //error: no matching function for call to 'foo'
}


Этот прием мы можем применить и для оригинального примера zaufi. Правда, придется добавить один фиктивный параметр ко второй перегрузке функции test — во избежание неоднозначности:
http://liveworkspace.org/code/1vnyrY$0
struct is_callable_with_checker
{
    typedef char yes_type;
    typedef char (&no_type)[2];

    template <typename...>
    static no_type test(...);

    // case #1 (original): simple add '*' to a deduced type to form a pointer type parameter
    template <typename Callable, typename... Args>
    static yes_type test(
        int, decltype(
            std::declval<Callable>()(std::forward<Args>(std::declval<Args>())...)
            , void()
          )* = nullptr
      );
};

template <typename Callable, typename... Args>
struct is_callable_with
  : std::is_same<
        decltype(is_callable_with_checker::template test<Callable, Args...>(int()))
      , is_callable_with_checker::yes_type
      >
{};


Очень интересная и поучительная тема. Спасибо!
--
Не можешь достичь желаемого — пожелай достигнутого.
Re[2]: Заставляем пример работать
От: andyp  
Дата: 05.03.13 17:43
Оценка: 8 (1)
Здравствуйте, rg45, Вы писали:

R>Очень интересная и поучительная тема. Спасибо!


Так работает без аргумента по умолчанию


// kate: hl C++11;
#include <iostream>
#include <type_traits>
#include <utility>

struct is_callable_with_checker
{
    typedef void* vp;
    typedef char yes_type;
    typedef char (&no_type)[2];

    template <typename...>
    static no_type test(...);

    // case #1 (original): simple add '*' to a deduced type to form a pointer type parameter
    template <typename Callable, typename... Args>
    static yes_type test(
        decltype(
            std::declval<Callable>()(std::forward<Args>(std::declval<Args>())...)
            , vp()
          )
      );
};

template <typename Callable, typename... Args>
struct is_callable_with
  : std::is_same<
        decltype(is_callable_with_checker::template test<Callable, Args...>(nullptr))
      , is_callable_with_checker::yes_type
      >
{};

// Some functor w/ overloaded operator(...)
struct test
{
    int operator()(int) const
    {
        return 123;
    }
    double operator()(int, double) const
    {
        return 123.123;
    }
};

int main()
{
    // expect 1
    std::cout << is_callable_with<test, int>::type::value << std::endl;
    // expect 1
    std::cout << is_callable_with<test, int, double>::type::value << std::endl;
    // expect 0
    std::cout << is_callable_with<test, int, std::ostream&>::type::value << std::endl;
}
Re[3]: Заставляем пример работать
От: rg45 СССР  
Дата: 05.03.13 18:17
Оценка:
Здравствуйте, andyp, Вы писали:

A>Так работает без аргумента по умолчанию


A>
  Скрытый текст
A>// kate: hl C++11;
A>#include <iostream>
A>#include <type_traits>
A>#include <utility>

A>struct is_callable_with_checker
A>{
A>    typedef void* vp;
A>    typedef char yes_type;
A>    typedef char (&no_type)[2];

A>    template <typename...>
A>    static no_type test(...);

A>    // case #1 (original): simple add '*' to a deduced type to form a pointer type parameter
A>    template <typename Callable, typename... Args>
A>    static yes_type test(
A>        decltype(
A>            std::declval<Callable>()(std::forward<Args>(std::declval<Args>())...)
A>            , vp()
A>          )
A>      );
A>};

A>template <typename Callable, typename... Args>
A>struct is_callable_with
A>  : std::is_same<
A>        decltype(is_callable_with_checker::template test<Callable, Args...>(nullptr))
A>      , is_callable_with_checker::yes_type
      >>
A>{};

A>// Some functor w/ overloaded operator(...)
A>struct test
A>{
A>    int operator()(int) const
A>    {
A>        return 123;
A>    }
A>    double operator()(int, double) const
A>    {
A>        return 123.123;
A>    }
A>};

A>int main()
A>{
A>    // expect 1
A>    std::cout << is_callable_with<test, int>::type::value << std::endl;
A>    // expect 1
A>    std::cout << is_callable_with<test, int, double>::type::value << std::endl;
A>    // expect 0
A>    std::cout << is_callable_with<test, int, std::ostream&>::type::value << std::endl;
A>}
A>


Да, забавно! Причем, вместо 'vp()' можно использовать что угодно: 'int()', double(), 'char()', '42', '3.14' — при этом вместо nullptr можно использовать то же выражение, которое было использовано вместо 'vp()', главное, чтоб по типу совпадало! А работает все тот же принцип, о котором я говорил выше: нужно лишить компилятора возможности выводить параметры шаблона из фактических параметров функции, и заставить его использовать параметры, указанные явно.
--
Не можешь достичь желаемого — пожелай достигнутого.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.