Форум » C/C++ » MS VC++ 2014: a std::accumulate's bug. » Ответить

MS VC++ 2014: a std::accumulate's bug.

Сыроежка: Реализация алгоритма std::accumulate в MS VC++ содержит неочевидный баг. Он неочевидный по той причине, что редко кто передает аргумент для параметра алгоритма init по ссылке, хотя это не воспрещается делать и на самом деле бывает очень удобно и эффективно с точки зрения работы алгоритма, так как не создаются временные объекты. Я думаю, что этот баг имеет место во всех версиях компилятора MS VC++. По крайней мере он присутствует в онлайновой версии компилятора от 2014 года, которая маркируется как Compiler version: 19.00.22318(x86) Last updated: Nov Вот программа, которая демонстрирует этот баг: [pre2] #include <iostream> #include <numeric> #include <iterator> int main() { int a[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; for ( int x : a ) std::cout << x << ' '; std::cout << std::endl; long long int acc = 0; std::cout << std::accumulate<decltype( std::begin( a ) ), long long int &> ( std::begin( a ), std::end( a ), acc ) << std::endl; std::cout << acc << std::endl; return 0; } [/pre2] Здесь параметр init, который инициализируется аргументом acc явно указан, как имеющий тип ссылки long long int &. Поэтому объект acc после работы алгоритма должен быть равен сумме элементов массива a. Однако данный код не компилируется компилятором MS VC++ из-за того, что реализация этого алгоритма в MS VC++ выполнена таким образом, что он вызывает другую внутреннюю функцию, где уже этот аргумент передается не по ссылке. Из-за этого и возникает ошибка компиляции. Эта же программа успешно компилируется как компилятором GCC, так и компилятором Clang. Я отослал сообщение о дефекте в Майкрософт, но последнее время там как-то вяло реагируют на подобные сообщения. То есть они остаются без ответа.

Ответов - 3

Сыроежка: Точно такая же проблема существует с реализацией алгоритма std::inner_product в MS VC++. То есть следующая программа не будет компилироваться: [pre2] #include <iostream> #include <numeric> #include <iterator> int main() { int a[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int b[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 }; for ( int x : a ) std::cout << x << ' '; std::cout << std::endl; for ( int x : b ) std::cout << x << ' '; std::cout << std::endl; std::cout << std::endl; long long int acc = 0; std::cout << std::inner_product<decltype( std::begin( a ) ), decltype( std::begin( b ) ), long long int &> ( std::begin( a ), std::end( a ), std::begin( b ), acc ) << std::endl; std::cout << acc << std::endl; return 0; }[/pre2] Хотя если запустить эту программу на www.ideone.com, используя компилятор GCC, или если запустить программу, используя clang 3.4, то она успешно скомпилируется и выведет на консоль следующий результат: [pre2] 0 1 2 3 4 5 6 7 8 9 9 8 7 6 5 4 3 2 1 0 120 120[/pre2]

Сыроежка: Можно говорить про баги в этих алгоритмах при условии, что аккумулятор внутри них имеет тот же тип, что и инициализатор init. Однако если почитать стандарт C++, то окажется, что нигде в описании этих алгоритмов не говорится о том, какой тип будет иметь аккумулятор внутри алгоритмов. Вот, например, пункт из стандарта C++, где говорится о принципе работы алгоритма std::accumulate: 1 Effects: Computes its result by initializing the accumulator acc with the initial value init and then modifies it with acc = acc + *i or acc = binary_op(acc, *i) for every iterator i in the range [first,last) in order. Конечно подразумевается, что тип аккумулятора точно такой же, как тип инициализатора init, иначе алгоритмы будут иметь неопределенное поведение. Представьте для примера, когда инициализатор при вызове std::accumulate имеет тип double, в то время как аккумулятор внутри алгоритма определяется как имеющий тип int. В этом случае результат работы алгоритма может быть не тем, что вы ожидаете. Очевидно, что в описании в стандарте C++ этих алгоритмов, std::accumulate и std::inner_product, следует явно указать, какой тип будет у аккумулятора, иначе это выглядит как дефект стандарта. Поэтому соответствующее предложение по исправлению описания этих алгоритмов в стандарте C++ я направлю в Комитет по стандартизации..

Сыроежка: Чтобы было наглядно видно, как определение внутреннего аккумулятора алгоритмов влияет на их поведение, можно рассмотреть следующий пример. Если, например, алгоритм std::accumulate определить следующим образом [pre2] template <class InputIterator, class T> T accumulate( InputIterator first, InputIterator last, T init ) { T acc = init; for ( ; first != last; ++first ) acc = acc + *first; return acc; }[/pre2] то нижеприведенная программа будет успешно компилироваться [pre2] #include <iostream> #include <iterator> template <class InputIterator, class T> T accumulate( InputIterator first, InputIterator last, T init ) { T acc = init; for ( ; first != last; ++first ) acc = acc + *first; return acc; } int main() { int a[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; long long acc = 0; accumulate<decltype( std::begin( a ) ), long long &> ( std::begin( a ), std::end( a ), acc ); std::cout << acc << std::endl; return 0; } [/pre2] и выведет на консоль значение 45. Ежели определить этот алгоритм таким образом, как [pre2] template <class InputIterator, class T> T accumulate( InputIterator first, InputIterator last, T init ) { auto acc = init; for ( ; first != last; ++first ) acc = acc + *first; return acc; } [/pre2] то данная программа (необходимо в нее подставить новое определение алгоритма вместо предыдущего) уже не выдаст ожидаемый результат. Так что очень важно, как аккумуляторы определяются внутри этих двух алгоритмов: std::accumulate и std::inner_product.




полная версия страницы