Какие гарантии порядка оценки введены в C++17?
Каковы последствия использования утвержденного порядка оценки C++17 (P0145) для типичного кода C++?
Что это меняет в таких вещах, как
i=1;
f(i++, i)
а также
std::cout << f() << f() << f() ;
или же
f(g(),h(),j());
2 ответа
Некоторые распространенные случаи, когда порядок оценки до сих пор не определен, указаны и действительны с C++17
, Некоторое неопределенное поведение теперь не указано.
Как насчет таких вещей, как
i=1; f(i++, i)
был неопределен, но теперь не указан.
std::cout << f() << f() << f() ;
Не определено, но станет совместимым с приоритетом оператора, так что первая оценка f
придет первым в потоке. (примеры ниже).
f(g(),h(),j());
все еще имеет неопределенный порядок оценки g, h, j. Обратите внимание, что для getf()(g(),h(),j())
правила гласят, что getf()
будет оцениваться раньше g,h,j
,
Также обратите внимание на следующий пример из текста предложения:
std::string s = "but I have heard it works even if you don't believe in it" s.replace(0, 4, "").replace(s.find("even"), 4, "only") .replace(s.find(" don't"), 6, "");
Пример взят из языка программирования C++, 4-е издание, Stroustrup, и имел обыкновение быть неопределенным поведением, но с C++17 он будет работать как положено. Были похожие проблемы с возобновляемыми функциями (.then( . . . )
).
В качестве другого примера рассмотрим следующее:
#include <iostream>
#include <string>
#include <vector>
#include <cassert>
struct Speaker{
int i =0;
Speaker(std::vector<std::string> words) :words(words) {}
std::vector<std::string> words;
std::string operator()(){
assert(words.size()>0);
if(i==words.size()) i=0;
// pre- C++17 version:
auto word = words[i] + (i+1==words.size()?"\n":",");
++i;
return word;
// Still not possible with C++17:
// return words[i++] + (i==words.size()?"\n":",");
}
};
int main() {
auto spk = Speaker{{"All", "Work", "and", "no", "play"}};
std::cout << spk() << spk() << spk() << spk() << spk() ;
}
С C++14 и раньше мы можем (и будем) получать такие результаты, как
play
no,and,Work,All,
вместо
All,work,and,no,play
Обратите внимание, что вышесказанное действует так же, как
(((((std::cout << spk()) << spk()) << spk()) << spk()) << spk()) ;
Но все же до C++17 не было никакой гарантии, что первые вызовы будут первыми в потоке.
Рекомендации: Из принятого предложения:
Постфиксные выражения оцениваются слева направо. Это включает вызовы функций и выражения выбора членов.
Выражения присваивания оцениваются справа налево. Это включает в себя составные задания.
Операнды для смещения операторов оцениваются слева направо. Таким образом, следующие выражения оцениваются в порядке a, затем b, затем c, затем d:
- аб
- а-> Ь
- а ->* б
- а (б1, б2, б3)
- б @= а
- а [Ь]
- а << б
- а >> б
Кроме того, мы предлагаем следующее дополнительное правило: порядок вычисления выражения, включающего перегруженный оператор, определяется порядком, связанным с соответствующим встроенным оператором, а не правилами для вызовов функций.
Редактировать заметку: мой оригинальный ответ неверно истолкован a(b1, b2, b3)
, Получатель чего-то b1
, b2
, b3
все еще не определено. (спасибо @KABoissonneault, все комментаторы.)
Тем не менее, (как указывает @Yakk), и это важно: даже когда b1
, b2
, b3
являются нетривиальными выражениями, каждое из них полностью вычисляется и привязывается к соответствующему параметру функции до того, как начнут оцениваться другие. Стандарт утверждает это следующим образом:
§5.2.2 - Вызов функции 5.2.2.4:
,,, Постфиксное выражение упорядочивается перед каждым выражением в списке выражений и любым аргументом по умолчанию. Каждое вычисление значения и побочный эффект, связанный с инициализацией параметра и самой инициализацией, упорядочивается перед каждым вычислением значения и побочным эффектом, связанным с инициализацией любого последующего параметра.
Тем не менее, одно из этих новых предложений отсутствует в проекте github:
Каждое вычисление значения и побочный эффект, связанный с инициализацией параметра и самой инициализацией, упорядочивается перед каждым вычислением значения и побочным эффектом, связанным с инициализацией любого последующего параметра.
Пример есть. Это решает давние проблемы ( как объяснил Херб Саттер) с исключительной безопасностью, где такие вещи, как
f(std::unique_ptr<A> a, std::unique_ptr<B> b);
f(get_raw_a(),get_raw_a());
протекает, если один из звонков get_raw_a()
бросил бы прежде, чем другой необработанный указатель был привязан к его параметру умного указателя edit: как указано в TC, пример ошибочен, так как конструкция unique_ptr из необработанного указателя является явной, что препятствует компиляции.
Также обратите внимание на этот классический вопрос (помеченный C, а не C++):
int x=0; x++ + ++x;
все еще не определено.
Чередование запрещено в C++17
В C++14 следующее было небезопасно:
void foo(std::unique_ptr<A>, std::unique_ptr<B> );
foo(std::unique_ptr<A>(new A), std::unique_ptr<B>(new B));
Есть четыре операции, которые происходят здесь во время вызова функции
new A
unique_ptr<A>
конструкторnew B
unique_ptr<B>
конструктор
Их порядок был совершенно не определен, поэтому вполне допустимым является порядок (1), (3), (2), (4). Если этот порядок был выбран и (3) выбрасывает, то память из (1) утечек - мы еще не запустили (2), что предотвратило бы утечку.
В C++17 новые правила запрещают чередование. Из [intro.execution]:
Для каждого вызова функции F, для каждой оценки A, которая происходит в пределах F, и каждой оценки B, которая не происходит в пределах F, но оценивается в том же потоке и как часть одного и того же обработчика сигнала (если таковой имеется), либо A последовательно перед B или B секвенируется перед A.
В этом предложении есть сноска, которая гласит:
Другими словами, выполнение функций не чередуется друг с другом.
Это оставляет нам два действительных порядка: (1), (2), (3), (4) или (3), (4), (1), (2). Не указано, какой порядок принят, но оба они безопасны. Все упорядочения, в которых (1) (3) происходят раньше (2) и (4), теперь запрещены.
Я нашел несколько заметок о порядке вычисления выражений:
- Быстрый вопрос: Почему в C++ нет определенного порядка для оценки аргументов функции?
Некоторый порядок оценки гарантирует, что перегруженные операторы и правила полного аргумента добавлены в C++17. Но остается то, что аргумент идет первым остается не определенным. В C++ 17 теперь указывается, что выражение, дающее то, что вызывать (код слева от (вызова функции), идет перед аргументами, и любой аргумент, вычисляемый первым, вычисляется полностью, прежде чем следующий запущен, и в случае метода объекта значение объекта оценивается до того, как аргументы метода будут.
- Порядок оценки
21) Каждое выражение в списке выражений, разделенных запятыми, в инициализаторе, заключенном в скобки, оценивается как для вызова функции (неопределенно-упорядоченный)
- Неоднозначные выражения
Язык C++ не гарантирует порядок, в котором оцениваются аргументы для вызова функции.
В P0145R3.Refining Порядок оценки выражений для Idiomatic C++ я нашел:
Вычисление значения и связанный с ним побочный эффект выражения postfix упорядочиваются раньше, чем выражения в списке выражений. Инициализация объявленных параметров выполняется неопределенным образом без чередования.
Но я не нашел его в стандарте, а в стандарте, который нашел:
6.8.1.8 Последовательное выполнение [intro.execution] Говорят, что выражение X упорядочено перед выражением Y, если каждое вычисление значения и каждый побочный эффект, связанный с выражением X, упорядочивается перед каждым вычислением значения и каждым побочным эффектом, связанным с выражением Y,
6.8.1.9 Последовательное выполнение [intro.execution] Каждое вычисление значения и побочный эффект, связанный с полным выражением, секвенируются перед каждым вычислением значения и побочным эффектом, связанным со следующим полным выражением, которое будет оценено.
7.6.19.1 Оператор запятой [expr.comma] Пара выражений, разделенных запятой, оценивается слева направо;...
Итак, я сравнил поведение в трех компиляторах для 14 и 17 стандартов. Исследуемый код:
#include <iostream>
struct A
{
A& addInt(int i)
{
std::cout << "add int: " << i << "\n";
return *this;
}
A& addFloat(float i)
{
std::cout << "add float: " << i << "\n";
return *this;
}
};
int computeInt()
{
std::cout << "compute int\n";
return 0;
}
float computeFloat()
{
std::cout << "compute float\n";
return 1.0f;
}
void compute(float, int)
{
std::cout << "compute\n";
}
int main()
{
A a;
a.addFloat(computeFloat()).addInt(computeInt());
std::cout << "Function call:\n";
compute(computeFloat(), computeInt());
}
Результаты (более последовательный лязг):
<style type="text/css">
.tg {
border-collapse: collapse;
border-spacing: 0;
border-color: #aaa;
}
.tg td {
font-family: Arial, sans-serif;
font-size: 14px;
padding: 10px 5px;
border-style: solid;
border-width: 1px;
overflow: hidden;
word-break: normal;
border-color: #aaa;
color: #333;
background-color: #fff;
}
.tg th {
font-family: Arial, sans-serif;
font-size: 14px;
font-weight: normal;
padding: 10px 5px;
border-style: solid;
border-width: 1px;
overflow: hidden;
word-break: normal;
border-color: #aaa;
color: #fff;
background-color: #f38630;
}
.tg .tg-0pky {
border-color: inherit;
text-align: left;
vertical-align: top
}
.tg .tg-fymr {
font-weight: bold;
border-color: inherit;
text-align: left;
vertical-align: top
}
</style>
<table class="tg">
<tr>
<th class="tg-0pky"></th>
<th class="tg-fymr">C++14</th>
<th class="tg-fymr">C++17</th>
</tr>
<tr>
<td class="tg-fymr"><br>gcc 9.0.1<br></td>
<td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
<td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
</tr>
<tr>
<td class="tg-fymr">clang 9</td>
<td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute float<br>compute int<br>compute</td>
<td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute float<br>compute int<br>compute</td>
</tr>
<tr>
<td class="tg-fymr">msvs 2017</td>
<td class="tg-0pky">compute int<br>compute float<br>add float: 1<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
<td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
</tr>
</table>