Имеет ли этот код из раздела "Язык программирования С++" раздел 36.3.6 четкого поведения?

В Бьярне Страуструпе C++ Раздел программирования 4-е издание 36.3.6 STL-подобные операции следующий код используется в качестве примера цепочки:

void f2()
{
    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, "" );

    assert( s == "I have heard it works only if you believe in it" ) ;
}

Сбой assert в gcc (посмотреть его вживую) и Visual Studio (посмотреть вживую), но он не завершится неудачей при использовании Clang (посмотреть вживую).

Почему я получаю разные результаты? Является ли какой-либо из этих компиляторов неправильно оценивающим выражением цепочки или этот код демонстрирует некоторую форму неопределенного или неопределенного поведения?

Ответ 1

Код демонстрирует неопределенное поведение из-за неопределенного порядка вычисления подвыражений, хотя он не вызывает неопределенное поведение, поскольку все побочные эффекты выполняются в функциях, которые вводят последовательность последовательности между побочными эффектами в этом случае.

Этот пример упоминается в предложении N4228: уточнение порядка оценки выражений для Idiomatic C++, в котором говорится следующее о коде в вопросе:

[...] Этот код был рассмотрен экспертами C++ по всему миру и опубликован ([Язык программирования C++, 4- е издание). Тем не менее, его уязвимость к неопределенному порядку оценки была обнаружена только недавно инструмент[...]

подробности

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

На первый взгляд может показаться, что, поскольку каждая replace должна оцениваться слева направо, соответствующие группы аргументов функции также должны оцениваться как группы слева направо.

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

s.find( "even" )

а также:

s.find( " don't" )

которые неопределенно упорядочены относительно:

s.replace(0, 4, "" )

два вызова find могут быть оценены до или после replace, что имеет значение, поскольку он имеет побочный эффект на s таким образом, что это повлияет на результат find, он изменит длину s. Таким образом, в зависимости от того, когда эта replace оценивается относительно двух вызовов find результат будет отличаться.

Если мы посмотрим на цепочечное выражение и рассмотрим порядок вычисления некоторых из подвыражений:

s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" )
^ ^       ^  ^  ^    ^        ^                 ^  ^
A B       |  |  |    C        |                 |  |
          1  2  3             4                 5  6

а также:

.replace( s.find( " don't" ), 6, "" );
 ^        ^                   ^  ^
 D        |                   |  |
          7                   8  9

Обратите внимание, мы игнорируем тот факт, что 4 и 7 могут быть разбиты на несколько подвыражений. Так:

  • A секвенируется перед B который секвенируется перед C который секвенируется перед D
  • Последовательности от 1 до 9 неопределенно упорядочены по отношению к другим подвыражениям с некоторыми исключениями, перечисленными ниже
    • 1 до 3 секвенируются до B
    • 4 до 6 секвенируют, прежде чем C
    • 7 по 9 секвенируются перед D

Ключ к этой проблеме заключается в том, что:

  • 4 по 9 являются неопределенно упорядоченными относительно B

Потенциальный порядок выбора оценки для 4 и 7 относительно B объясняет разницу в результатах между clang и gcc при оценке f2(). В моих тестах clang оценивает B до оценки 4 и 7 а gcc после. Мы можем использовать следующую тестовую программу, чтобы продемонстрировать, что происходит в каждом случае:

#include <iostream>
#include <string>

std::string::size_type my_find( std::string s, const char *cs )
{
    std::string::size_type pos = s.find( cs ) ;
    std::cout << "position " << cs << " found in complete expression: "
        << pos << std::endl ;

    return pos ;
}

int main()
{
   std::string s = "but I have heard it works even if you don't believe in it" ;
   std::string copy_s = s ;

   std::cout << "position of even before s.replace(0, 4, \"\" ): " 
         << s.find( "even" ) << std::endl ;
   std::cout << "position of  don't before s.replace(0, 4, \"\" ): " 
         << s.find( " don't" ) << std::endl << std::endl;

   copy_s.replace(0, 4, "" ) ;

   std::cout << "position of even after s.replace(0, 4, \"\" ): " 
         << copy_s.find( "even" ) << std::endl ;
   std::cout << "position of  don't after s.replace(0, 4, \"\" ): "
         << copy_s.find( " don't" ) << std::endl << std::endl;

   s.replace(0, 4, "" ).replace( my_find( s, "even" ) , 4, "only" )
        .replace( my_find( s, " don't" ), 6, "" );

   std::cout << "Result: " << s << std::endl ;
}

Результат для gcc (посмотреть вживую)

position of even before s.replace(0, 4, "" ): 26
position of  don't before s.replace(0, 4, "" ): 37

position of even after s.replace(0, 4, "" ): 22
position of  don't after s.replace(0, 4, "" ): 33

position  don't found in complete expression: 37
position even found in complete expression: 26

Result: I have heard it works evenonlyyou donieve in it

Результат для clang (посмотреть вживую):

position of even before s.replace(0, 4, "" ): 26
position of  don't before s.replace(0, 4, "" ): 37

position of even after s.replace(0, 4, "" ): 22
position of  don't after s.replace(0, 4, "" ): 33

position even found in complete expression: 22
position don't found in complete expression: 33

Result: I have heard it works only if you believe in it

Результат для Visual Studio (посмотреть его вживую):

position of even before s.replace(0, 4, "" ): 26
position of  don't before s.replace(0, 4, "" ): 37

position of even after s.replace(0, 4, "" ): 22
position of  don't after s.replace(0, 4, "" ): 33

position  don't found in complete expression: 37
position even found in complete expression: 26
Result: I have heard it works evenonlyyou donieve in it

Детали из стандарта

Мы знаем, что если не указано, вычисления подвыражений не являются последовательными, то это из черновика проекта стандарта C++ 11, раздел 1.9 Выполнение программы", в котором говорится:

Если не указано иное, вычисления операндов отдельных операторов и подвыражений отдельных выражений не являются последовательными. [...]

и мы знаем, что вызов функции вводит последовательность перед тем, как связь функции вызывает выражение postfix и аргументы относительно тела функции, из раздела 1.9:

[...] При вызове функции (независимо от того, является ли функция встроенной), каждое вычисление значения и побочный эффект, связанные с любым выражением аргумента или с выражением постфикса, обозначающим вызываемую функцию, упорядочиваются перед выполнением каждого выражения или оператора. в теле вызываемой функции. [...]

Мы также знаем, что доступ к членам класса и, следовательно, цепочка будут оцениваться слева направо, из раздела 5.2.5 Доступ к членам класса, который гласит:

[...] Выражение постфикса перед точкой или стрелкой оценивается; 64 результат этой оценки вместе с выражением id определяет результат всего выражения postfix.

Обратите внимание, что в случае, когда id-выражение оказывается нестатической функцией-членом, оно не определяет порядок вычисления списка выражений в () поскольку это отдельное подвыражение. Соответствующая грамматика из 5.2 выражений Postfix:

postfix-expression:
    postfix-expression ( expression-listopt)       // function call
    postfix-expression . templateopt id-expression // Class member access, ends
                                                   // up as a postfix-expression

C++ 17 изменений

Предложение p0145r3: уточнение порядка оценки выражений для Idiomatic C++ внесло несколько изменений. Включая изменения, которые дают коду хорошо определенное поведение, усиливая порядок правил оценки для выражений postfix и их списка выражений.

[expr.call] p5 говорит:

Постфиксное выражение упорядочивается перед каждым выражением в списке выражений и любым аргументом по умолчанию. Инициализация параметра, включая каждое связанное с ним вычисление значения и побочный эффект, определяется неопределенным образом относительно последовательности любого другого параметра. [Примечание: все побочные эффекты при оценке аргументов секвенируются до входа в функцию (см. 4.6). —Конец примечания] [Пример:

void f() {
std::string s = "but I have heard it works even if you dont believe in it";
s.replace(0, 4, "").replace(s.find("even"), 4, "only").replace(s.find(" dont"), 6, "");
assert(s == "I have heard it works only if you believe in it"); // OK
}

- конец примера]

Ответ 2

Это предназначено для добавления информации по этому вопросу в отношении С++ 17. Предложение (Refining Expression Evaluation Order для Idiomatic С++ Revision 2) для C++17 рассмотрело вопрос со ссылкой на приведенный выше код в качестве образца.

Как я уже сказал, я добавил соответствующую информацию из предложения и процитировал (подчеркивает мой):

Порядок оценки выражения, как он в настоящее время указан в стандарте, подрывает советы, популярные идиомы программирования или относительную безопасность стандартных библиотечных объектов. Ловушки предназначены не только для новичков или небрежным программистом. Они затрагивают всех нас без разбора, даже когда мы знаем правила.

Рассмотрим следующий фрагмент программы:

void f()
{
  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, "");
  assert(s == "I have heard it works only if you believe in it");
}

Утверждение должно проверять предполагаемый результат программиста. Он использует "цепочку" вызовов функций-членов, общую стандартную практику. Этот код был рассмотрен экспертами С++ по всему миру и опубликован (язык программирования С++, 4-е издание). Однако его уязвимость для неуказанного порядка оценки была обнаружена только недавно инструментом.

В документе предлагается изменить правило pre-t20 > на порядок оценки выражения, на которое повлияло C и существовало более трех десятилетий. Он предложил, чтобы язык должен гарантировать современные идиомы или подвергать риску "ловушки и источники неясности, трудно найти ошибки", например, что произошло с образцом кода выше.

Предложение C++17 заключается в требовать, чтобы каждое выражение имело четко определенный порядок оценки:

  • Постфиксные выражения оцениваются слева направо. Сюда входят вызовы функций и выражения выбора членов.
  • Выражения присваивания оцениваются справа налево. Сюда входят составные назначения.
  • Операторы для переключения операторов вычисляются слева направо.
  • Порядок оценки выражения, связанного с перегруженным оператором, определяется порядком, связанным с соответствующим встроенным оператором, а не правилами для вызовов функций.

Вышеприведенный код успешно компилируется с использованием GCC 7.1.1 и Clang 4.0.0.