Условный оператор, чай баг - мой или gcc ?
От: ScorpZ Украина  
Дата: 28.05.13 09:06
Оценка: 2 (1)
1  #include <iostream>
2  #include <functional>
3
4  typedef std::function<void (int)> ffPtrT;
5
6  void f1(int i) {}
7
8  void call(ffPtrT fPtr)
9  {
10   if(fPtr != nullptr)
11   {
12     std::cout << ">>> TRUE !!!" << std::endl;
13     fPtr(10);                    // Sigfault here
14   }
15   else
16   {
17     std::cout << ">>> FALSE !!!" << std::endl;
18   }
19 }
20
21 int main(int argc, char** argv)
22 {
23   bool setCall = false;
24   call((setCall ? &f1 : nullptr));
25 }

//------------------------------------------------------- Console
./test_c
>>> TRUE !!!
Segmentation fault (core dumped)
//------------------------------------------------------- Console

Всем привет.
Столкнулся с таким загадочным поведением gcc.
Вообщем есть такая программка, скомпиленая на gcc 4.7.2 с поддержкой 11 стандарта и некоторыми его фичами (std::function).
Если следовать логике, то в тирнарном операторе должен вычислятся правый операнд (nullptr) и он же должен передаваться в call(),
где значение nullptr используется для инициализации fPtr. По факту — в fPtr попадаетя какой то мусор, который совсем не равен nullptr.

Да, и еще — gcc, при компиляции, не выдает никаких ошибок или ворнингов.

В итоге починилоь все таким образом —
24   call((setCall ? ffPtrT(&f1) : ffPtrT(nullptr)));  
// на самом деле все работает даже если только для 2го операнда создавать временный указатель
// то есть

24   call((setCall ? ffPtrT(&f1) : nullptr));

//------------------------------------------------------- Console
./test_c
>>> FALSE !!!
//------------------------------------------------------- Console

А теперь вопрос знатокам С++ — что это было ?
добавлена разметка
Re: Условный оператор, чай баг - мой или gcc ?
От: Mr.Delphist  
Дата: 28.05.13 09:19
Оценка:
Здравствуйте, ScorpZ, Вы писали:

Пожалуйста, переформатируйте код, глаза на лоб лезут читать этот дамп

SZ>А теперь вопрос знатокам С++ — что это было ?


Наугад: скорее всего, обычный для тернарного оператора подводный камень — оба операнда должны иметь одинаковый тип (или приводиться к нему), иначе вывод типа попытается сделать компилятор, а далее уж как повезёт, обычная рулетка type casting.
Re[2]: Условный оператор, чай баг - мой или gcc ?
От: ScorpZ Украина  
Дата: 28.05.13 10:48
Оценка:
Здравствуйте, Mr.Delphist, Вы писали:

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


MD>Пожалуйста, переформатируйте код, глаза на лоб лезут читать этот дамп


SZ>>А теперь вопрос знатокам С++ — что это было ?


MD>Наугад: скорее всего, обычный для тернарного оператора подводный камень — оба операнда должны иметь одинаковый тип (или приводиться к нему), иначе вывод типа попытается сделать компилятор, а далее уж как повезёт, обычная рулетка type casting.


Я тоже думал в этом направлении.
На мсдн (кстати не знаю как этот код будет компилиться и работать на VC++), по поводу условного оператора, написано следующее —
"If both expressions are of pointer types or if one is a pointer type and the other is a constant expression that evaluates to 0, pointer conversions are performed to convert them to a common type."
Получается, что gcc пытается nullptr привести к указателю на ф-цию, а получается у него это не очень.
Re[2]: Условный оператор, чай баг - мой или gcc ?
От: ScorpZ Украина  
Дата: 28.05.13 11:28
Оценка:
Здравствуйте, Mr.Delphist, Вы писали:

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


MD>Пожалуйста, переформатируйте код, глаза на лоб лезут читать этот дамп


SZ>>А теперь вопрос знатокам С++ — что это было ?


MD>Наугад: скорее всего, обычный для тернарного оператора подводный камень — оба операнда должны иметь одинаковый тип (или приводиться к нему), иначе вывод типа попытается сделать компилятор, а далее уж как повезёт, обычная рулетка type casting.


Сорри, не нашел где редактировать предидущее сообщение.
И еще заметка по теме, если вот этот модерный тип — std::function поменять на простой указатель на ф-цию, то все прекрасно работает.
Re: Условный оператор, чай баг - мой или gcc ?
От: Nikita.Trophimov  
Дата: 28.05.13 11:48
Оценка:
Что примечательно:

#include <functional>
#include <iostream>

int main()
{
    typedef void (*fp_t)();

    fp_t fp = fp_t();
    if (fp == nullptr)
    {
        std::cout << "true \n";
    }
    else
    {
        std::cout << "false \n";
    }
}


Output

true


#include <functional>
#include <iostream>

int main()
{
    typedef void (*fp_t)();

    std::function<void()> fp = fp_t();
    if (fp == nullptr)
    {
        std::cout << "true \n";
    }
    else
    {
        std::cout << "false \n";
    }
}


Output

false

Re[3]: Условный оператор, чай баг - мой или gcc ?
От: Mr.Delphist  
Дата: 28.05.13 11:51
Оценка:
Здравствуйте, ScorpZ, Вы писали:

SZ>Сорри, не нашел где редактировать предидущее сообщение.

Ну, можно ответом самому себе, только с тегом ccode и без номеров строк

SZ>И еще заметка по теме, если вот этот модерный тип — std::function поменять на простой указатель на ф-цию, то все прекрасно работает.

Ну так std::function — это класс с оператором "скобочки", а попытка превратить адрес экзмпляра этого класса в указатель на свободную функцию громко упадёт.
Re: Условный оператор, чай баг - мой или gcc ?
От: k.o. Россия  
Дата: 28.05.13 14:56
Оценка:
Здравствуйте, ScorpZ, Вы писали:

SZ> Всем привет.

SZ>Столкнулся с таким загадочным поведением gcc.
SZ>Вообщем есть такая программка, скомпиленая на gcc 4.7.2 с поддержкой 11 стандарта и некоторыми его фичами (std::function).
SZ>Если следовать логике, то в тирнарном операторе должен вычислятся правый операнд (nullptr) и он же должен передаваться в call(),
SZ>где значение nullptr используется для инициализации fPtr. По факту — в fPtr попадаетя какой то мусор, который совсем не равен nullptr.

SZ> Да, и еще — gcc, при компиляции, не выдает никаких ошибок или ворнингов.


это бага gcc, твой код (+ реализация std::function из gcc) можно свести к такому:

#include <iostream>

     template<typename _Tp>
       static bool
       _M_not_empty_function(const _Tp*& __fp)
       { return __fp; }
 
     template<typename _Tp>
       static bool
       _M_not_empty_function(const _Tp&)
       { return true; }

template <class T>
bool check(T t)
{
    return _M_not_empty_function(t);
}

int main()
{
    typedef void (*fp_t)();

    auto result = check(fp_t());
    std::cout << result << "\n";
}


При этом, очевидно, будет вызвана перегрузка _M_not_empty_function, которая всегда возвращает 'true', поскольку аналогичный код используется std::function, созданный объект будет считать, что у него есть ненулевой указатель на функцию.

SZ>В итоге починилоь все таким образом —


SZ>24 call((setCall ? ffPtrT(&f1) : ffPtrT(nullptr)));

SZ>// на самом деле все работает даже если только для 2го операнда создавать временный указатель
SZ>// то есть

SZ>24 call((setCall ? ffPtrT(&f1) : nullptr));


А это работает правильно, т.к. у std::function (по стандарту) есть специальный конструктор принимающий nullptr_t, который, естественно, никаких проверок не делает, а сразу инициализирует пустой функтор.
Re[2]: Условный оператор, чай баг - мой или gcc ?
От: Nikita.Trophimov  
Дата: 28.05.13 15:04
Оценка:
KO>это бага gcc, твой код (+ реализация std::function из gcc) можно свести к такому:

Тогда это и баг MSVC-11.0 тоже.
Re[2]: Условный оператор, чай баг - мой или gcc ?
От: Nikita.Trophimov  
Дата: 28.05.13 15:15
Оценка:
Попробую собрать мысли воедино

Получается, что:

— Как известно, conditional operator требует, чтобы переданные ему второй и третий операнды были одного и того же типа. В случае несовпадения типов он будет пытаться преобразовать один из них к другому. В данном случае он, видимо, решает преобразовать nullptr к указателю на функцию (не уверен, не могу найти точного доказательства в стандарте)
— Если не ошибаюсь, далее мы имеем примерно следующую ситуацию:

#include <functional>
#include <iostream>

int main()
{
    typedef void (*fp_t)();

    std::function<void()> fp = fp_t();
    if (fp == nullptr)
    {
        std::cout << "true \n";
    }
    else
    {
        std::cout << "false \n";
    }
}


Данный код выведет в stdout "false" (также не уверен, почему именно)

Что происходит? Кто-нибудь может помочь, пожалуйста?
Re[3]: Условный оператор, чай баг - мой или gcc ?
От: k.o. Россия  
Дата: 28.05.13 15:23
Оценка:
Здравствуйте, Nikita.Trophimov, Вы писали:

NT>Попробую собрать мысли воедино


NT>Получается, что:


NT>- Как известно, conditional operator требует, чтобы переданные ему второй и третий операнды были одного и того же типа. В случае несовпадения типов он будет пытаться преобразовать один из них к другому. В данном случае он, видимо, решает преобразовать nullptr к указателю на функцию (не уверен, не могу найти точного доказательства в стандарте)

NT>- Если не ошибаюсь, далее мы имеем примерно следующую ситуацию:
  Скрытый текст
NT>
NT>#include <functional>
NT>#include <iostream>

NT>int main()
NT>{
NT>    typedef void (*fp_t)();

NT>    std::function<void()> fp = fp_t();
NT>    if (fp == nullptr)
NT>    {
NT>        std::cout << "true \n";
NT>    }
NT>    else
NT>    {
NT>        std::cout << "false \n";
NT>    }
NT>}
NT>


Да, тут все верно.

NT>Данный код выведет в stdout "false" (также не уверен, почему именно)


NT>Что происходит? Кто-нибудь может помочь, пожалуйста?


Так я же, вроде, объяснил. std::function проверят переданный указатель на функция с помощью набора перегруженных методов '_M_not_empty_function'. Один из них принимает ссылку на std::function, другой — указатель на функцию-член класса, эти перегрузки, очевидно не подходят, остаются еще 2, которые я приводил. Из этих перегрузок выбирается та, которая всегда возвращает 'true', поэтому std::function считает, что передан ненулевой указатель.
Re[3]: Условный оператор, чай баг - мой или gcc ?
От: ScorpZ Украина  
Дата: 28.05.13 15:25
Оценка: -1
Здравствуйте, Nikita.Trophimov, Вы писали:

NT>Попробую собрать мысли воедино


NT>Получается, что:


NT>- Как известно, conditional operator требует, чтобы переданные ему второй и третий операнды были одного и того же типа. В случае несовпадения типов он будет пытаться преобразовать один из них к другому. В данном случае он, видимо, решает преобразовать nullptr к указателю на функцию (не уверен, не могу найти точного доказательства в стандарте)

NT>- Если не ошибаюсь, далее мы имеем примерно следующую ситуацию:

NT>
NT>#include <functional>
NT>#include <iostream>

NT>int main()
NT>{
NT>    typedef void (*fp_t)();

NT>    std::function<void()> fp = fp_t();
NT>    if (fp == nullptr)
NT>    {
NT>        std::cout << "true \n";
NT>    }
NT>    else
NT>    {
NT>        std::cout << "false \n";
NT>    }
NT>}
NT>


NT>Данный код выведет в stdout "false" (также не уверен, почему именно)


NT>Что происходит? Кто-нибудь может помочь, пожалуйста?


Да , вот фраза из MSDN —

If both expressions are of pointer types or if one is a pointer type and the other is a constant expression that evaluates to 0, pointer conversions are performed to convert them to a common type.
получается , что для выражения —
// (setCall ? &f1 : nullptr)
"common type" как раз и будет указатель на ф-цию. То есть при преобразовании значения типа (std::nullptr_t)
в (void(*)(int)) где то и происходит сбой.
Re: Условный оператор, чай баг - мой или gcc ?
От: Кодт Россия  
Дата: 28.05.13 19:36
Оценка:
Здравствуйте, ScorpZ, Вы писали:

Минимальный пример:
#include <cassert>
#include <functional>

typedef void FUN(int);
typedef FUN* FUNPTR;
typedef std::function<FUN> ffPtrT;

int main()
{
  assert(!ffPtrT(nullptr));
  assert(!ffPtrT((FUNPTR)nullptr)); // assertion failed
}

Поскольку в тернарном операторе (cond ? &f1 : nullptr) универсальный std::nullptr_t конкретизируется до типизированного FUNPTR, — получаем то, что получаем.

Вместо ffPtrT(nullptr) в ветке тернарного оператора можно написать просто ffPtrT().
Перекуём баги на фичи!
Re[2]: Условный оператор, чай баг - мой или gcc ?
От: ScorpZ Украина  
Дата: 28.05.13 20:35
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Вместо ffPtrT(nullptr) в ветке тернарного оператора можно написать просто ffPtrT().

Та ну — ffPtrT(nullptr) как то нагляднее,
... А теперь самое интересное, запустил я свой пример на VC++ 2010 , все работает правильно и замечательно !!!
Таки gcc что то намудрили.
Re[3]: Условный оператор, чай баг - мой или gcc ?
От: Nikita.Trophimov  
Дата: 28.05.13 20:44
Оценка:
SZ>... А теперь самое интересное, запустил я свой пример на VC++ 2010 , все работает правильно и замечательно !!!
SZ>Таки gcc что то намудрили.

В MSVC-10.0 да, а вот MSVC-11.0 действует в этом плане так же, как и gcc 4.7.2.
Re: Условный оператор, чай баг - мой или gcc ?
От: FrozenHeart  
Дата: 30.05.13 19:55
Оценка:
Было бы неплохо, если бы Вы снабжали свои сообщения ссылками на какой-нибудь онлайн-компилятор — не у всех под рукой имеется обычная версия. Т.к. на LWS до сих пор нельзя выкладывать новые куски кода, сделаем это на ideone — тут.

Насколько я понял, происходит вот что.

Как уже было неоднократно замечено, в случае несовпадения типов второго и третьего операндов conditional operator'а, компилятор попытается привести один из них к другому согласно тем правилам, что указаны в 5.16 Conditional operator [expr.cond]. Если быть более конкретным, случай, описанный в теме, относится к следующему (выделял самостоятельно):

ISO/IEC 14882:2011

5.16 Conditional operator [expr.cond]

6 Lvalue-to-rvalue (4.1), array-to-pointer (4.2), and function-to-pointer (4.3) standard conversions are performed
on the second and third operands. After those conversions, one of the following shall hold:

[...]

— The second and third operands have pointer type, or one has pointer type and the other is a null
pointer constant
, or both are null pointer constants, at least one of which is non-integral; pointer
conversions (4.10) and qualification conversions (4.4) are performed to bring them to their composite
pointer type (5.9). The result is of the composite pointer type.


Посмотрим, что из себя представляет composite type, и как он получается в данном случае (выделял самостоятельно):

ISO/IEC 14882:2011

5.9 Relational operators [expr.rel]

2 [...] If one operand is a null pointer constant, the composite
pointer type is std::nullptr_t if the other operand is also a null pointer constant or, if the other operand
is a pointer, the type of the other operand
.


Значит, nullptr приводится к типу указателя на функцию. В связи с этим, исходный пример можно переписать следующим образом:

#include <functional>
#include <iostream>

int main()
{
    typedef void (*fp_t)();

    fp_t fp = nullptr;
    std::function<void()> f = fp;
    std::cout << (f == nullptr) << '\n';
}


Как и в случае с исходным примером, данный код также выведет "0" в gcc 4.7.2 и MSVC-11.0.

Посмотрим, что из себя представляет operator== для данных типов в стандарте:

ISO/IEC 14882:2011

20.8.11.2.6 null pointer comparison operators [func.wrap.func.nullptr]

template <class R, class... ArgTypes>
bool operator==(const function<R(ArgTypes...)>& f, nullptr_t) noexcept;
template <class R, class... ArgTypes>
bool operator==(nullptr_t, const function<R(ArgTypes...)>& f) noexcept;
1 Returns: !f.


Теперь посмотрим, как мы конструировали объект класса std::function (выделял самостоятельно):

ISO/IEC 14882:2011

20.8.11.2.1 function construct/copy/destroy [func.wrap.func.con]

template<class F> function(F f);
template <class F, class A> function(allocator_arg_t, const A& a, F f);

[...]

8 Postconditions: !*this if any of the following hold:
f is a NULL function pointer.
— f is a NULL pointer to member.
— F is an instance of the function class template, and !f


Т.к. fp из примера выше является null function pointer, то, согласно этой цитате, гарантируется, что в результате конструирования объекта выражение !f, которое как раз и используется для сравнения с nullptr при помощи operator==, будет равняться true.

На основе этих данных можно полагать, что это баг. Воспроизводится как минимум на gcc 4.7.2 и MSVC-11.0. Работает так, как ожидается (т.е. выводит "1"), в MSVC-10.0.

По этому поводу уже был открыт соответствующий баг-репорт.

Разумеется, это уже детали реализации, но если кому интересно, что происходит в случае gcc, попробуем посмотреть.

Внутри мы имеем следующие перегруженные версии функции _M_not_empty_function:

template<typename _Signature>
  static bool
  _M_not_empty_function(const function<_Signature>& __f)
  { return static_cast<bool>(__f); }

template<typename _Tp>
  static bool
  _M_not_empty_function(const _Tp*& __fp)
  { return __fp; }

template<typename _Class, typename _Tp>
  static bool
  _M_not_empty_function(_Tp _Class::* const& __mp)
  { return __mp; }

template<typename _Tp>
  static bool
  _M_not_empty_function(const _Tp&)
  { return true; }


Возможно, что предполагалось то, что функция

template<typename _Tp>
  static bool
  _M_not_empty_function(const _Tp*& __fp)
  { return __fp; }


будет вызываться для указателей на функцию, но на деле оказалось не так. В случаях, подобных тому, что приведён в исходном коде темы, будет вызываться перегрузка функции, которая всегда возвращает true:

template<typename _Tp>
  static bool
  _M_not_empty_function(const _Tp&)
  { return true; }


Между прочим, boost::function ведёт себя точно так же в случае сравнения с nullptr:

#include <boost/function.hpp>

#include <iostream>

int main()
{
    typedef void (*fp_t)();

    fp_t fp = nullptr;
    boost::function<void()> f = fp;
    std::cout << (f == nullptr) << '\n';
}


В отличие от std::function, в документации к конструктору boost::function не говорится явно, что postcondition'ом в случае null pointer function является !*this. Вместо этого они гарантируют, что функция-член empty будет возвращать true.

template<typename F> function(F f);

Requires:
F is a function object Callable from this.

Postconditions:
*this targets a copy of f if f is nonempty, or this->empty() if f is empty.


Исходный пример можно переписать, например, вот так:

#include <boost/function.hpp>

#include <iostream>

typedef boost::function<void(int)> fp_t;

void bar(int) {}

void baz(fp_t fp)
{
    if (!fp.empty())
    {
        std::cout << "true \n";
        fp(0);
    }
    else
    {
        std::cout << "false \n";
    }
}

int main()
{
    bool foo = false;
    baz(foo ? &bar : nullptr);
}


Или так:

#include <boost/function.hpp>

#include <iostream>

typedef boost::function<void(int)> fp_t;

void bar(int) {}

void baz(fp_t fp)
{
    if (fp)
    {
        std::cout << "true \n";
        fp(0);
    }
    else
    {
        std::cout << "false \n";
    }
}

int main()
{
    bool foo = false;
    baz(foo ? &bar : nullptr);
}


http://www.boost.org/doc/libs/1_53_0/doc/html/boost/function.html#idp41743568-bb

operator safe_bool() const;

Returns:
A safe_bool that evaluates false in a boolean context when this->empty(), and true otherwise.

Throws:
Will not throw.

 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.