Имеет ли этот код из раздела 36.3.6 4-го издания «Язык программирования C ++» четко определенное поведение?

94

В разделе « Операции, подобные STL» Бьярна Страуструпа The C ++ Programming Language 4th edition, в качестве примера цепочки используется следующий код :36.3.6

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" ) ;
}

Утверждение терпит неудачу gcc( смотрите вживую ) и Visual Studio( смотрите вживую ), но оно не терпит неудачу при использовании Clang ( смотрите вживую ).

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

Шафик Ягмур
источник
Лучше:s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );
Бен Фойгт
20
Помимо ошибки, я единственный, кто считает, что такого уродливого кода не должно быть в книге?
Karoly Horvath
5
@KarolyHorvath Обратите внимание, что cout << a << b << coperator<<(operator<<(operator<<(cout, a), b), c)лишь немного менее уродливо.
Oktalist 01
1
@Oktalist: :) по крайней мере, у меня есть намерение. он учит поиску имени, зависящему от аргумента, и синтаксису оператора одновременно в кратком формате ... и не создает впечатления, что вы действительно должны писать такой код.
Karoly Horvath

Ответы:

104

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

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

[...] Этот код был проверен экспертами C ++ по всему миру и опубликован (The C ++ Programming Language, 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 Выполнение программы», в котором говорится:

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

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

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

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

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

Обратите внимание, что в случае, когда 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: Уточнение порядка оценки выражений для идиоматического C ++ внесло несколько изменений. Включая изменения, которые придают коду четко заданное поведение за счет усиления порядка правил оценки для постфиксных выражений и их списка выражений .

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

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

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"); // OK
}

—Конечный пример]

Шафик Ягмур
источник
7
Я немного удивлен, увидев, что «многие эксперты» упустили эту проблему, хорошо известно, что оценка постфиксного выражения вызова функции не является последовательной до оценки аргументов (во всех версиях C и C ++).
MM
@ShafikYaghmour Вызовы функций неопределенно упорядочены относительно друг друга и всего остального, за исключением отмеченных вами отношений «упорядочено до». Тем не менее, оценка 1, 2, 3, 5, 6, 8, 9, "even", "don't"и несколько случаев sявляются unsequenced по отношению друг к другу.
TC
4
@TC нет, это не так (вот как возникает эта "ошибка"). Например foo().func( bar() ), он мог позвонить foo()как до, так и после звонка bar(). Постфикс-выражение является foo().func. Аргументы и постфиксное выражение располагаются перед телом func(), но не по порядку относительно друг друга.
MM
@MattMcNabb Ах, да, я неправильно понял. Вы говорите о самом постфиксном выражении , а не о вызове. Да, верно, они неупорядочены (если, конечно, не применяется какое-то другое правило).
TC
6
Также есть фактор, который склонен считать, что код, появляющийся в книге Б.Строуструпа, верен, иначе кто-то наверняка уже заметил бы! (связанные; пользователи SO все еще находят новые ошибки в K&R)
MM
4

Это предназначено для добавления информации по этому вопросу в отношении C ++ 17. Предложение ( Порядок уточнения выражения для идиоматической версии C ++ 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");
}

Утверждение должно подтверждать предполагаемый результат программиста. Он использует «цепочку» вызовов функций-членов, что является обычной стандартной практикой. Этот код был проверен экспертами по C ++ во всем мире и опубликован (The C ++ Programming Language, 4-е издание). Тем не менее, его уязвимость к неопределенному порядку оценки была обнаружена инструментом только недавно.

В документе предлагается изменить предварительное C++17правило по порядку оценки выражений, которое существует под влиянием Cи существует уже более трех десятилетий. Было предложено, чтобы язык гарантировал современные идиомы или рисковал «ловушками и источниками непонятных, труднообнаруживаемых ошибок». таких как то, что произошло с примером кода выше.

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

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

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

Рики М
источник