Стандартизировать порядок вычисления аргументов переданных в функцию

Игорь
Игорь

Давным-давно просто был ужасно опечален, узнав что в моем любимом С++ не стандартизован порядок исполнения аргументов у функции.

Я готов мириться со многими UB, но с таким не хочется. Про него писал еще вроде Майерс, но я так и не понял, почему никто не хочет взяться и стандартизировать такую важную вещь? Причем, в С++17 какие то были фиксы в сторону порядка вычислений каких-то выражений. Сейчас, скомпилив этот код ниже в последнем шланге и последнем gcc, я получаю совершенно разный вывод. Во многих проектах я неоднократно натыкался на этот UB. И я уверен, что натыкались на это все С++ разработчики. Объясните, что мешает урегулировать данное поведение, чтобы все вычислялось слева направо? Я так понял, изначально при проектировании С++ на этот вопрос не уделили должного внимания. Спасибо. Вот код который ведет себя по разному в разных компиляторах:

 

#include <iostream>
int foo() {
    std::cout << "foo\n"; return 42;
}

int bar() {
    std::cout << "bar\n"; return 42;
}

void some(int, int) { }

int main() {
    some(foo(), bar());
}

 

Один компилятор выводит:

foo
bar

Другой:

bar
foo

Пока мне виднеется эта проблема сверх критической в языке.

 

-5
рейтинг
13 комментариев
Andrey Davydov
В С++17 обязали компилятор вычислять аргументы последовательно, т.е. вычисление i-го аргумента не может перемежаться с вычислением j-го (где i < j или i > j стандарт не говорит). Это очень полезное изменение, так как оно гарантирует, что у нас не будет одновременно нескольких частично сконструированных объектов. Но то в каком порядке аргументы будут вычисляться не фиксировано, и это очень правильно ведь в зависимости от calling convention аргументы в разном порядке кладутся в стэк. То есть, если потребовать вычислять аргументы функции слево направо, а при этом в стэк они будут начиная с последнего, то достичь этого можно будет только ценой очевидной пессимизации.
То что Вы называете undefined behavior на самом деле unspecified behavior -- очень большая разница, веть от него корректность программы отнюдь не страдает.
Andrey Davydov
Игорь
Andrey Davydov, ничего подобного, корректность страдает. В функциональных языках программирования, в которых у функций гарантируется чистота - это бы да, сработало. В С++ же, функции могут влиять на внешний мир, к тому же, этот язык императивный, и с точки зрения логики наблюдаемого кода - должен быть порядок вычисления аргументов. Да, сейчас порядок есть, но нет направления, но все мы читаем слева направо и пишем сверху вниз. Инструкции выполняются в логическом порядке. Только не в параметрах функций, функторов, конструкторов - здесь логика исполнения кода нарушается. В одних компиляторах слева направо, в других справа на лево. Вы ошибаетесь что корректность не страдает, можно привести тысячу примеров, где страдает
Игорь
Andrey Davydov
rumyancev95, Вы проигнорировали почти весь мой комментарий в котором, я объяснял, почему Ваше предложение ударит по производительности, зато вцепились в последнее словосочетание "корректность не страдает", вырвав его из контекста :)
"Корректность программы не страдает", было написано в том контексте, что Вы неверно употребили термин UB -- неправда что все программы на C++ вызывающие функции больше чем с одним параметром содержат UB, а значит некорректны.
Действительно, программы заточенные под unspecified behavior (скажем, порядок вычисления аргументов) *логически* некорректны. Это не значит, что надо запретить в стандарте любое unspecified behavior, ведь оно там не просто так, это значит, что если в каком-то частном случае порядок вычисления аргументов принципиален, то обеспечить его ответственность программиста.
Andrey Davydov
Игорь
Andrey Davydov, в чем потеря производительности? Шланг выводит по порядку, а гцц нет. Следовательно гцц производительнее шланга в данном вопросе?
Игорь
Andrey Davydov
rumyancev95, Вы ассемблер не предъявили, но к запихиванию аргументов в стэк Ваш пример не имеет никакого отношения -- очевидно что пустая функция `some` должна быть заинлайнена, и даже если этого и не происходит, то аргументы будут переданы через регистры.
Andrey Davydov
Павел Корозевцев
rumyancev95, боюсь, в вашем случае аргументы вообще в регистры легли, поэтому порядок не важен в смысле производительности складывания на стек. но плюшки x86_64 не позволяют нам не думать о производительности на других платформах.
Павел Корозевцев
Игорь
Павел Корозевцев, Если там и есть какие то вопросы по произведительности - ок, я не против. Но почему то поведение clang, в отличии от gcc меня полностью устраивает, так как есть логический порядок. Даже если там и действительно будут проблемы по производительности, то лично по мне, должны быть очень весомые причины, чтобы нарушать логический порядок. Я хочу, чтобы мне кто-нибудь дал внятный ответ, почему стандартом не может быть регламентирован порядок и направление вычисления аргументов. Вот один из тысячи кейсов, когда этот порядок необходим:
Допустим у меня есть итератор на какой-нибудь кэш, и у него есть метод next() который двигает итератор вправо. И есть функция, которая принимает два каких-то поля из экземпляра кэша:
auto it = cash.getIterator();
process(it.next()->getName(), it->getId());
В данном случае, если нет стандартизированного порядка вычислений, у нас в метод может попасть два поля из разных экземпляров кэша.
Да, можно было бы написать вот так, и все бы заработало:
auto it = cash.getIterator();
it.next();
process(it->getName(), it->getId());
Но как вы понимаете, всем хочется писать код короче. Да даже без стремления к красткости кода, можно много где наткнуться на эту проблему. Ведь есть еще всякие фабрики, функции модифицирующие указатель, выделающие память и т.п. Примеров привести можно множество.
Игорь
Antervis
rumyancev95, у вас результат одной операции зависит от побочного эффекта другой - не кошер
Antervis
Игорь
Antervis, Antervis, Какой побочный эффект? Вы в коде разобрались? .next() пердвигает итератор вправо, он просто двигает итератор на позицию вперед. Следовательно, после смещения, дальнейшие операции будут применены к следующему объекту.
Игорь
Дмитрий
rumyancev95, по жизни я с таким не сталкивался. Полагаю в таких редких, исключительных ситуациях, позволительно писать код в 2 строки.
Дмитрий
Antervis
rumyancev95, next() - который помимо возврата итератора изменяет состояние объекта it. Это и называется "побочный эффект".
Antervis
languagelawyer

> Это очень полезное изменение, так как оно гарантирует, что у нас не будет одновременно нескольких частично сконструированных объектов.

А до этого изменения могли быть такие объекты?
Вычисления двух функций не могут перемежаться, даже если функции заинлайнены. Для конструкторов это правило не действует?

languagelawyer
Andrey

languagelawyer, возможно, я криво выразился, это была отсылка к классическому примеру о пользе `std::make_unique`:

foo(unique_ptr<A>(new A), unique_ptr<B>(new B));

В этом коде до C++17 возможен такой порядок: `new A`, `new B` -- и если он кидает исключение, то объект `A` утечет. Проблема решалась так:

foo(make_unique<A>(), make_unique<B>());

Но в C++17 нет самой проблемы, так как гарантируется, что сразу после `new A` сконструируется `unique_ptr<A>`, т.е. вычисление 1-го и 2-го аргумента `foo` перемежаться не будет.

Andrey
Обновлено 
Другие идеи
Группа создана, чтобы собирать предложения к стандарту C++, организовывать их внутренние обсуждения, помогать готовить их для отправки в комитет и защищать на общих собраниях в рабочей группе по С++ Международной организации по стандартизации (ISO).
Все предложения