Czy o bug STL dla Visual studio?

0

Hej w jakimś projekcie natrafiłęm na dziwny problem:
Ten kod:

#include <iostream>
#include <functional>

using namespace std;

typedef std::function<void()> CallBack;

class TestChangeCallback
{
public:
	void Do(const int& a)
	{
		cout << "Before: "  << a << endl;
		m_Callback = std::bind(&TestChangeCallback::Do, this, 2);
		cout << "After: "  << a << endl;
	}

	void Invoke()
	{
		cout << __FUNCTION__ << " start " << bool(m_Callback)<< endl;
		if (m_Callback)
		{
			m_Callback();
		}
		cout << __FUNCTION__ << " end " << bool(m_Callback)<< endl;
	}

	CallBack m_Callback;
};

int main() {
	TestChangeCallback test;

	test.m_Callback = std::bind(&TestChangeCallback::Do, &test, 1);
	test.Invoke();
	cout << __FUNCTION__ << endl;

	return 0;
}

daje taki wynik na gcc/clang:

Invoke start 1
Before: 1
After: 1
Invoke end 1
main

http://ideone.com/QQ93Ld

a Visual Studio daje taki wynik (testowałem tutaj bo jestem microsoftowo upośledzony):

TestChangeCallback::Invoke start 1
Before: 1
After: 2
TestChangeCallback::Invoke end 1
main

Jeśli to bug, to czy znany?
Wartość const się zmienia więc raczej, bug.

4

Jak dla mnie to co robisz to UB. bind kopiuje swoje argumenty, więc a w TestChangeCallback::Do jest referencją do zmiennej, której czas życia jest równy z czasem życia m_Callback. W momencie gdy przypisujesz nową wartość do m_Callback, czas życia starej się kończy, a więc nie masz prawa już użyć a. Założę się (a zaraz to sprawdzę), że jeśli w gcc/clangu przepuścisz to z ubsanem to dostaniesz informację o use-after-free. W tym momencie otrzymywane wyniki są efektem detali implementacji dla std::function, gdzie MSVC korzysta z czegoś analogicznego do SSO (stąd adres a się nie zmienia), a clang i gcc alokują na stosie.

[pts/0:krzaq@ArchVM:~/tmp]% g++ -fsanitize=address -g marek.cpp
[pts/0:krzaq@ArchVM:~/tmp]% ./a.out
Invoke start 1
Before: 1
=================================================================
==10150==ERROR: AddressSanitizer: heap-use-after-free on address 0x60300000eff0 at pc 0x000000401657 bp 0x7ffdc2815410 sp 0x7ffdc2815400
READ of size 4 at 0x60300000eff0 thread T0
    #0 0x401656 in TestChangeCallback::Do(int const&) /home/krzaq/tmp/marek.cpp:15
    #1 0x402e3a in void std::__invoke_impl<void, void (TestChangeCallback::* const&)(int const&), TestChangeCallback*&, int&>(std::__invoke_memfun_deref, void (TestChangeCallback::* const&)(int const&), TestChangeCallback*&, int&) /usr/include/c++/6.1.1/functional:235
    #2 0x402cbc in std::result_of<void (TestChangeCallback::* const&(TestChangeCallback*&, int&))(int const&)>::type std::__invoke<void (TestChangeCallback::* const&)(int const&), TestChangeCallback*&, int&>(void (TestChangeCallback::* const&)(int const&), TestChangeCallback*&, int&) /usr/include/c++/6.1.1/functional:260
    #3 0x402c50 in decltype (__invoke((*this)._M_pmf, (forward<TestChangeCallback*&>)({parm#1}), (forward<int&>)({parm#1}))) std::_Mem_fn_base<void (TestChangeCallback::*)(int const&), true>::operator()<TestChangeCallback*&, int&>(TestChangeCallback*&, int&) const /usr/include/c++/6.1.1/functional:613
    #4 0x402ba4 in void std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)>::__call<void, , 0ul, 1ul>(std::tuple<>&&, std::_Index_tuple<0ul, 1ul>) /usr/include/c++/6.1.1/functional:943
    #5 0x4028b3 in void std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)>::operator()<, void>() /usr/include/c++/6.1.1/functional:1002
    #6 0x40211b in std::_Function_handler<void (), std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)> >::_M_invoke(std::_Any_data const&) /usr/include/c++/6.1.1/functional:1740
    #7 0x401a53 in std::function<void ()>::operator()() const /usr/include/c++/6.1.1/functional:2136
    #8 0x4016fe in TestChangeCallback::Invoke() /home/krzaq/tmp/marek.cpp:23
    #9 0x401328 in main /home/krzaq/tmp/marek.cpp:35
    #10 0x7f133fe43740 in __libc_start_main (/usr/lib/libc.so.6+0x20740)
    #11 0x401168 in _start (/home/krzaq/tmp/a.out+0x401168)

0x60300000eff0 is located 16 bytes inside of 32-byte region [0x60300000efe0,0x60300000f000)
freed by thread T0 here:
    #0 0x7f1340b2db30 in operator delete(void*, unsigned long) /build/gcc/src/gcc/libsanitizer/asan/asan_new_delete.cc:108
    #1 0x402aa9 in std::_Function_base::_Base_manager<std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)> >::_M_destroy(std::_Any_data&, std::integral_constant<bool, false>) /usr/include/c++/6.1.1/functional:1595
    #2 0x402207 in std::_Function_base::_Base_manager<std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)> >::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation) /usr/include/c++/6.1.1/functional:1619
    #3 0x40151b in std::_Function_base::~_Function_base() /usr/include/c++/6.1.1/functional:1699
    #4 0x401761 in std::function<void ()>::~function() /usr/include/c++/6.1.1/functional:1843
    #5 0x4019d3 in std::enable_if<std::function<void ()>::_Callable<std::decay<std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)> >::type, std::result_of<std::decay<std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)> >::type ()>::type>::value, std::function<void ()>&>::type std::function<void ()>::operator=<std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)> >(std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)>&&) /usr/include/c++/6.1.1/functional:2001
    #6 0x40161f in TestChangeCallback::Do(int const&) /home/krzaq/tmp/marek.cpp:14
    #7 0x402e3a in void std::__invoke_impl<void, void (TestChangeCallback::* const&)(int const&), TestChangeCallback*&, int&>(std::__invoke_memfun_deref, void (TestChangeCallback::* const&)(int const&), TestChangeCallback*&, int&) /usr/include/c++/6.1.1/functional:235
    #8 0x402cbc in std::result_of<void (TestChangeCallback::* const&(TestChangeCallback*&, int&))(int const&)>::type std::__invoke<void (TestChangeCallback::* const&)(int const&), TestChangeCallback*&, int&>(void (TestChangeCallback::* const&)(int const&), TestChangeCallback*&, int&) /usr/include/c++/6.1.1/functional:260
    #9 0x402c50 in decltype (__invoke((*this)._M_pmf, (forward<TestChangeCallback*&>)({parm#1}), (forward<int&>)({parm#1}))) std::_Mem_fn_base<void (TestChangeCallback::*)(int const&), true>::operator()<TestChangeCallback*&, int&>(TestChangeCallback*&, int&) const /usr/include/c++/6.1.1/functional:613
    #10 0x402ba4 in void std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)>::__call<void, , 0ul, 1ul>(std::tuple<>&&, std::_Index_tuple<0ul, 1ul>) /usr/include/c++/6.1.1/functional:943
    #11 0x4028b3 in void std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)>::operator()<, void>() /usr/include/c++/6.1.1/functional:1002
    #12 0x40211b in std::_Function_handler<void (), std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)> >::_M_invoke(std::_Any_data const&) /usr/include/c++/6.1.1/functional:1740
    #13 0x401a53 in std::function<void ()>::operator()() const /usr/include/c++/6.1.1/functional:2136
    #14 0x4016fe in TestChangeCallback::Invoke() /home/krzaq/tmp/marek.cpp:23
    #15 0x401328 in main /home/krzaq/tmp/marek.cpp:35
    #16 0x7f133fe43740 in __libc_start_main (/usr/lib/libc.so.6+0x20740)

previously allocated by thread T0 here:
    #0 0x7f1340b2ce30 in operator new(unsigned long) /build/gcc/src/gcc/libsanitizer/asan/asan_new_delete.cc:60
    #1 0x4027f7 in std::_Function_base::_Base_manager<std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)> >::_M_init_functor(std::_Any_data&, std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)>&&, std::integral_constant<bool, false>) /usr/include/c++/6.1.1/functional:1656
    #2 0x4020f0 in std::_Function_base::_Base_manager<std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)> >::_M_init_functor(std::_Any_data&, std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)>&&) /usr/include/c++/6.1.1/functional:1627
    #3 0x401fca in std::function<void ()>::function<std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)>, void, void>(std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)>) /usr/include/c++/6.1.1/functional:2123
    #4 0x4019b4 in std::enable_if<std::function<void ()>::_Callable<std::decay<std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)> >::type, std::result_of<std::decay<std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)> >::type ()>::type>::value, std::function<void ()>&>::type std::function<void ()>::operator=<std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)> >(std::_Bind<std::_Mem_fn<void (TestChangeCallback::*)(int const&)> (TestChangeCallback*, int)>&&) /usr/include/c++/6.1.1/functional:2001
    #5 0x40131c in main /home/krzaq/tmp/marek.cpp:34
    #6 0x7f133fe43740 in __libc_start_main (/usr/lib/libc.so.6+0x20740)

SUMMARY: AddressSanitizer: heap-use-after-free /home/krzaq/tmp/marek.cpp:15 in TestChangeCallback::Do(int const&)
Shadow bytes around the buggy address:
  0x0c067fff9da0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9db0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9dc0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9dd0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9de0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x0c067fff9df0: fa fa fa fa fa fa 00 00 00 00 fa fa fd fd[fd]fd
  0x0c067fff9e00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9e10: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9e20: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9e30: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9e40: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Heap right redzone:      fb
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack partial redzone:   f4
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==10150==ABORTING

Jeśli main zmienisz na:

int main() {
    TestChangeCallback test;
 
    int n = 1;
    test.m_Callback = std::bind(&TestChangeCallback::Do, &test, ref(n));
    test.Invoke();
    cout << __FUNCTION__ << endl;
 
    return 0;
}

To nie ma żadnych błędów i działa zgodnie z oczekiwaniami.

0

Nie wiem czy to bug, i czy w Visualu czy w GCC ;-)
Ale mogę potwierdzić że „problem” występuje pod VS 2015 i pod VS 2008.

Występuje też pod Boostem (1.60) niezależnie od kompilatora, więc jeśli szukasz jednolitego zachowania niezależnie od platformy, to boost::function i boost::bind ;-)

0

To prawdopodobnie UB, gdyż nie ma gwarancji umieszczenia nowej wartości parametru pod adresem starej.
Jeżeli zaś chodzi o const referencję, to wskazywana wartość jak najbardziej może się zmieniać:

#include <iostream>
 
using namespace std;
 
class TestChangeCallback
{
public:
    void Do(const int& a)
    {
        cout << "Before: "  << a << endl;
        b = 2;
        cout << "After: "  << a << endl;
    }
 
    void Invoke()
    {
        Do(b);
    }
 
    int b = 1;
};
 
int main() {
    TestChangeCallback test;
    test.Invoke();
 
    return 0;
} 
0
Pebal napisał(a):

Jeżeli zaś chodzi o const referencję, to wskazywana wartość jak najbardziej może się zmieniać

Do tego nie trzeba nawet klasy.

int i = 2;
const int &r = i;

cout << "before " << r << endl;
i = 4;
cout << "after " << r << endl;
0

Dzięki za naświetlenie problemu. Nie pomyślałem o UB.

Oczywiście właściwy kod jest bardziej skomplikowany i to sprzężenie zwrotne nie jest takie ciasne.
Mam napisany na to test, puszczę go pod OS X i włączę "zombies objects" powinno wykryć ten błąd.

Dzięki waszym uwagom pookładało mi się bardziej to w głowie. Oczekiwałem od std::function jakiejś ochrony na czas wykonania, np że dane są utrzymywane poprzez shared_ptr (lub coś podobnego), który jest podtrzymywany podczas wykonania. W końcu nie wiadomo ile danych przechowuje ta zmienna, bo bind (albo lambda) może wstawić tam cokolwiek, więc dane muszą być trzymane na stercie.
Patrząc na to w ten sposób można oczekiwać, że to nie powinno być UB.

Dzięki.

1 użytkowników online, w tym zalogowanych: 0, gości: 1