C ++ 17에서 도입 된 평가 순서 보증은 무엇입니까?


95

일반적인 C ++ 코드 에서 C ++ 17 평가 순서 보증 (P0145) 에서 투표 한 결과는 어떤 의미 입니까?

다음과 같은 점에서 무엇이 변경됩니까?

i = 1;
f(i++, i)

std::cout << f() << f() << f();

또는

f(g(), h(), j());

관련 C에 할당 문의 평가의 주문 ++행동을 잘 정의 "언어 프로그래밍 C ++"4 판 섹션 36.3.6에서이 코드는 무엇입니까? 둘 다 종이에 덮여 있습니다. 첫 번째는 아래 답변에서 멋진 추가 예제를 만들 수 있습니다.
Shafik Yaghmour

또한 다소 관련이 있습니다 : 연산자 오버로딩 함수가있는 C ++ 17 평가 순서 .
dfrib

답변:


83

지금까지 평가 순서가 지정되지 않은 몇 가지 일반적인 경우 는에서 지정되고 유효합니다 C++17. 일부 정의되지 않은 동작은 이제 대신 지정되지 않습니다.

i = 1;
f(i++, i)

정의되지 않았지만 이제는 지정되지 않았습니다. 특히 지정되지 않은 것은 각 인수가 f다른 인수와 비교하여 평가 되는 순서입니다 . i++이전 i에 평가 되거나 그 반대로 평가 될 수 있습니다 . 실제로 동일한 컴파일러에 있음에도 불구하고 다른 순서로 두 번째 호출을 평가할 수 있습니다.

그러나, 각 인자의 평가된다 필요한 다른 모든 인자의 실행 전에 모든 부작용으로 완전히 실행. 따라서 f(1, 1)(두 번째 인수가 먼저 평가됨) 또는 f(1, 2)(첫 번째 인수가 먼저 평가됨 )을 얻을 수 있습니다 . 그러나 당신은 f(2, 2)그 성질의 어떤 것도 얻지 못할 것입니다.

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

이 예제는 The C ++ Programming Language , 4th edition, 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. ab
  2. a-> b
  3. a-> * b
  4. a (b1, b2, b3)
  5. b @ = a
  6. a [b]
  7. a << b
  8. a >> b

또한 다음과 같은 추가 규칙을 제안합니다. 오버로드 된 연산자를 포함하는 표현식의 평가 순서는 함수 호출 규칙이 아닌 해당 내장 연산자와 관련된 순서에 의해 결정됩니다.

메모 수정 : 내 원래 답변이 잘못 해석되었습니다 a(b1, b2, b3). 의 순서는 b1, b2, b3아직 지정되지 않습니다. (@KABoissonneault, 모든 댓글 작성자에게 감사드립니다.)

그러나, (@Yakk는 지적으로)와이 중요하다하더라도 b1, b2, b3, 사소하지 않은 표현입니다 그들 각각이 완전하게 평가 하고, 각각의 함수 매개 변수에 묶여 다른 사람이 평가되기 시작되기 전에. 표준은 다음과 같이 설명합니다.

§5.2.2-함수 호출 5.2.2.4 :

. . . postfix-expression은 expression-list 및 기본 인수의 각 표현식 앞에 순서가 지정됩니다. 매개 변수 초기화와 관련된 모든 값 계산 및 부작용 및 초기화 자체는 모든 후속 매개 변수의 초기화와 관련된 모든 값 계산 및 부작용 이전에 시퀀싱됩니다.

그러나 이러한 새로운 문장 중 하나가 GitHub 초안 에서 누락되었습니다 .

매개 변수 초기화와 관련된 모든 값 계산 및 부작용 및 초기화 자체는 모든 후속 매개 변수의 초기화와 관련된 모든 값 계산 및 부작용 이전에 시퀀싱됩니다.

예는 것입니다 있다. 그것은 다음 과 같은 예외 안전 으로 수십 년 된 문제를 해결합니다 ( Herb Sutter에서 설명 )

f(std::unique_ptr<A> a, std::unique_ptr<B> b);

f(get_raw_a(), get_raw_a());

get_raw_a()다른 원시 포인터가 스마트 포인터 매개 변수에 연결되기 전에 호출 중 하나 가 throw 되면 누출됩니다 .

TC가 지적했듯이 원시 포인터의 unique_ptr 구성이 명시 적이므로 컴파일을 방지하기 때문에 예제에 결함이 있습니다. *

또한이 고전주의 질문 (태그 C 가 아니라 C는 ++ ) :

int x=0;
x++ + ++x;

아직 정의되지 않았습니다.


1
"두 번째 보조 제안은 함수 호출의 평가 순서를 다음과 같이 대체합니다. 함수는 모든 인수보다 먼저 평가되지만 인수 목록의 인수 쌍은 결정되지 않게 순서가 지정됩니다. 즉, 하나는 다른 인수보다 먼저 평가되지만 순서가 지정되지 않았습니다. 인수 전에 함수가 평가된다는 것이 보장됩니다. 이는 핵심 작업 그룹의 일부 구성원이 제안한 내용을 반영합니다. "
Yakk-Adam Nevraumont

1
나는 "다음과 같은 표현이 순서대로 평가됩니다 말하는 종이에서 그 느낌을 받고 있어요 a, 다음 b, 다음 c, 다음, d다음 보여주는" a(b1, b2, b3)모든 것을 건의 b(이 될 것이다, 그렇지 않으면 표현이 반드시 임의의 순서로 평가되지 않습니다 a(b, c, d))
KABoissonneault

1
@KABoissoneault, 당신은 정확하고 그에 따라 답변을 업데이트했습니다. 또한 모든 인용문 내가 이해하는 한 버전에서 투표 된 버전 3입니다.
Johan Lundberg

2
@JohanLundberg 논문에서 제가 중요하다고 생각하는 또 다른 것이 있습니다. a(b1()(), b2()())주문 수 b1()()b2()()순서에 상관없이,하지만 수 없습니다b1()다음 b2()()다음 b1()(): 그것은 더 이상 실행 인터리브 할 수있다. 요컨대, "8. 기능 호출을위한 대체 평가 명령"은 승인 된 변경의 일부였습니다.
Yakk-Adam Nevraumont

3
f(i++, i)정의되지 않았습니다. 이제 지정되지 않았습니다. Stroustrup의 문자열 예제는 정의되지 않은 것이 아니라 지정되지 않았을 것입니다. `f (get_raw_a (), get_raw_a ());`는 관련 unique_ptr생성자가 명시 적이므로 컴파일되지 않습니다 . 마지막으로 x++ + ++x정의되지 않은 기간입니다.
TC

44

인터리빙은 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));

함수 호출 중에 여기에서 발생하는 네 가지 작업이 있습니다.

  1. new A
  2. unique_ptr<A> 건설자
  3. new B
  4. unique_ptr<B> 건설자

이들의 순서는 완전히 지정되지 않았으므로 완벽하게 유효한 순서는 (1), (3), (2), (4)입니다. 이 순서를 선택하고 (3) 던지면 (1)의 메모리가 누출됩니다. (2) 아직 실행하지 않았으므로 누출을 방지 할 수 있습니다.


C ++ 17에서 새로운 규칙은 인터리빙을 금지합니다. [intro.execution]에서 :

각 함수 호출 F에 대해 F 내에서 발생하는 모든 평가 A 및 F 내에서 발생하지 않지만 동일한 스레드에서 평가되고 동일한 신호 처리기 (있는 경우)의 일부로 평가되는 모든 평가 B에 대해 A가 B보다 먼저 시퀀싱됩니다. 또는 B가 A보다 먼저 시퀀싱됩니다.

그 문장에는 다음과 같은 각주가 있습니다.

즉, 함수 실행은 서로 인터리브되지 않습니다.

이로 인해 (1), (2), (3), (4) 또는 (3), (4), (1), (2)의 두 가지 유효한 순서가 남습니다. 어떤 순서를 취하는지는 명시되어 있지 않지만 둘 다 안전합니다. (1) (3) 둘 다 (2)와 (4) 이전에 발생하는 모든 주문은 이제 금지됩니다.


1
약간 제쳐두고, 이것은 boost :: make_shared 및 나중에 std :: make_shared의 이유 중 하나였습니다 (다른 이유는 더 적은 할당 + 더 나은 지역성). 예외 안전 / 자원 유출 동기가 더 이상 적용되지 않는 것처럼 들립니다. 코드 예제 3, boost.org/doc/libs/1_67_0/libs/smart_ptr/doc/html/… 편집stackoverflow.com/a/48844115 , herbsutter.com/2013/05/29/gotw-89-solution- 참조 스마트 포인터
최대 Barraclough

3
이 변경이 최적화에 어떤 영향을 미치는지 궁금합니다. 컴파일러는 이제 인수 계산과 관련된 CPU 명령을 결합하고 인터리브하는 방법에 대한 옵션 수를 크게 줄 였으므로 CPU 사용률이 낮아질 수 있습니까?
Violet Giraffe

2

식 평가 순서에 대한 몇 가지 참고 사항을 찾았습니다.

  • 빠른 Q : 왜 C ++에는 함수 인수를 평가하기위한 지정된 순서가 없습니까?

    일부 평가 순서는 오버로드 된 연산자와 C ++ 17에 추가 된 완전한 인수 규칙을 보장합니다. 그러나 어떤 주장이 먼저 진행되는지는 명시되지 않은 채로 남아 있습니다. C ++ 17에서는 호출 할 항목을 제공하는 표현식 ((함수 호출의 왼쪽에있는 코드)이 인수 앞에오고 먼저 평가되는 인수가 다음 인수가되기 전에 완전히 평가되도록 지정되었습니다. 시작되고 객체 메소드의 경우 객체의 값이 메소드에 대한 인수보다 먼저 평가됩니다.

  • 평가 순서

    21) 괄호로 묶인 이니셜 라이저에서 쉼표로 구분 된 표현식 목록의 모든 표현식은 함수 호출 ( indeterminately-sequenced ) 처럼 평가됩니다.

  • 모호한 표현

    C ++ 언어는 함수 호출에 대한 인수가 평가되는 순서를 보장하지 않습니다.

에서 관용적 C에 대한 식 평가 순서 P0145R3.Refining ++ 나는 발견했습니다 :

postfix-expression의 값 계산 및 관련 부작용은 expression-list의 표현식보다 먼저 시퀀싱됩니다. 선언 된 매개 변수의 초기화는 인터리빙없이 불확실하게 시퀀스 됩니다.

그러나 나는 그것을 표준에서 찾지 못했습니다. 대신 표준에서 찾았습니다.

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

결과 (일관된 것이 clang) :

<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>

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.