program tip

C ++ 함수에서 정확한 "반환"순간

radiobox 2020. 11. 12. 08:05
반응형

C ++ 함수에서 정확한 "반환"순간


어리석은 질문처럼 보이지만 return xxx;함수에서 "실행" 되는 정확한 순간 이 명확하게 정의되어 있습니까?

내가 의미하는 바를 보려면 다음 예를 참조하십시오 ( 여기 라이브 ).

#include <iostream>
#include <string>
#include <utility>

//changes the value of the underlying buffer
//when destructed
class Writer{
public:
    std::string &s;
    Writer(std::string &s_):s(s_){}
    ~Writer(){
        s+="B";
    }
};

std::string make_string_ok(){
    std::string res("A");
    Writer w(res);
    return res;
}


int main() {
    std::cout<<make_string_ok()<<std::endl;
} 

내가 순진하게 일어날 것으로 기대하는 make_string_ok것은 다음과 같습니다.

  1. 의 생성자 res가 호출 됨 (의 값 res"A")
  2. 생성자 w가 호출 됨
  3. return res실행됩니다. res의 현재 값 (의 현재 값을 복사하여 res), 즉를 반환해야합니다 "A".
  4. 에 대한 소멸자 w가 호출되면의 값 res"AB".
  5. 에 대한 소멸자 res가 호출됩니다.

따라서 "A"결과를 기대 하지만 "AB"콘솔에 인쇄됩니다.

반면에 약간 다른 버전의 경우 make_string:

std::string make_string_fail(){
    std::pair<std::string, int> res{"A",0};
    Writer w(res.first);
    return res.first;
}

결과는 예상대로입니다 "A"( live 참조 ).

표준은 위의 예에서 반환되어야하는 값을 규정합니까 아니면 지정되지 않았습니까?


시각적 인 동작을 변경할 수있는 최적화 중 하나 인 RVO (+ 사진을 안개로 만드는 임시 복사본으로 반환)입니다.

10.9.5 복사 / 이동 제거 (강조는 내 것임 ) :

특정 기준이 충족되면 복사 / 이동 작업을 위해 선택한 생성자 및 / 또는 객체에 대한 소멸자가 부작용 **이 있더라도 구현시 클래스 객체의 복사 / 이동 구성을 생략 할 수 있습니다. 이러한 경우 구현은 생략 된 복사 / 이동 작업의 소스 및 대상을 동일한 객체를 참조하는 두 가지 다른 방법으로 취급합니다 .

복사 제거라고하는 이러한 복사 / 이동 작업 제거는 다음과 같은 상황에서 허용됩니다 (여러 복사본을 제거하기 위해 결합 될 수 있음).

  • 클래스 반환 유형이있는 함수의 return 문에서 표현식이 동일한 유형 ( 처리기의 예외 선언에 의해 도입 된 함수 매개 변수 또는 변수 제외)의 비 휘발성 자동 객체 이름 인 경우 cv-qualification 무시) 함수 반환 유형 으로 자동 객체를 함수 호출의 반환 객체에 직접 구성하여 복사 / 이동 작업을 생략 할 수 있습니다.
  • [...]

적용 여부에 따라 전체 전제가 잘못됩니다. 1. c'tor for res가 호출되지만 객체는 내부 make_string_ok또는 외부에 있을 수 있습니다 .

사례 1.

글 머리 기호 2와 3은 전혀 발생하지 않을 수도 있지만 이것은 측면 포인트입니다. 대상이 영향을 Writer받은 s dtor의 부작용을 얻었습니다 make_string_ok. 이것은 make_string_ok평가의 맥락에서 를 사용하여 임시로 생성되었습니다 operator<<(ostream, std::string). 컴파일러는 임시 값을 만든 다음 함수를 실행했습니다. 이는 임시 생활이 외부에 있기 때문에 중요하므로의 대상 Writer은 로컬이 make_string_ok아니라에 operator<<있습니다.

사례 2.

한편, 두 번째 예는 유형이 다르기 때문에 기준에 맞지 않습니다 (간결성을 위해 생략 된 것도 아닙니다). 그래서 작가는 죽습니다. .NET Framework의 일부인 경우 죽을 수도 pair있습니다. 따라서 여기에서의 복사본이 res.first임시 객체로 반환되고 dtor of Writerres.first자체적으로 죽게 될 원본 영향 줍니다 .

copy에 의해 반환 된 객체도 파괴되기 때문에 소멸자를 호출하기 전에 복사본이 만들어 졌다는 것은 꽤 분명해 보입니다. 그렇지 않으면 복사 할 수 없습니다.

결국 그것은 Writer최적화가 적용되는지 여부에 따라 외부 객체 또는 로컬 객체 에서 d' tor가 작동 하기 때문에 RVO로 귀결됩니다 .

표준은 위의 예에서 반환되어야하는 값을 규정합니까 아니면 지정되지 않았습니까?

아니요, 최적화는 선택 사항이지만 관찰 가능한 동작을 변경할 수 있습니다. 적용 여부는 컴파일러의 재량입니다. 컴파일러가 관찰 가능한 동작을 변경하지 않는 모든 변환을 수행 할 수 있도록 허용하는 "일반적인 as-if"규칙에서 제외됩니다.

A case for it became mandatory in c++17, but not yours. The mandatory one is where the return value is an unnamed temporary.


Due to Return Value Optimization (RVO), a destructor for std::string res in make_string_ok may not be called. The string object can be constructed on the caller's side and the function may only initialize the value.

The code will be equivalent to:

void make_string_ok(std::string& res){
    Writer w(res);
}

int main() {
    std::string res("A");
    make_string_ok(res);
}

That is why the value return shall be "AB".

In the second example, RVO does not apply, and the value will be copied to the returned value exactly upon the call to return, and Writer's destructor will run on res.first after the copy occurred.

6.6 Jump statements

On exit from a scope (however accomplished), destructors (12.4) are called for all constructed objects with automatic storage duration (3.7.2) (named objects or temporaries) that are declared in that scope, in the reverse order of their declaration. Transfer out of a loop, out of a block, or back past an initialized variable with automatic storage duration involves the destruction of variables with automatic storage duration that are in scope at the point transferred from...

...

6.6.3 The Return Statement

The copy-initialization of the returned entity is sequenced before the destruction of temporaries at the end of the full-expression established by the operand of the return statement, which, in turn, is sequenced before the destruction of local variables (6.6) of the block enclosing the return statement.

...

12.8 Copying and moving class objects

31 When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization.(123) This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

— in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cvunqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value

123) Because only one object is destroyed instead of two, and one copy/move constructor is not executed, there is still one object destroyed for each one constructed.


There is a concept in C++ called elision.

Elision takes two seemingly distinct objects and merges their identity and lifetime.

Prior to elision could occur:

  1. When you have a non-parameter variable Foo f; in a function that returned Foo and the return statement was a simple return f;.

  2. When you have an anonymous object being used to construct pretty much any other object.

In all (almost?) cases of #2 are eliminated by the new prvalue rules; elision no longer occurs, because what used to create a temporary object no longer does so. Instead, the construction of the "temporary" is directly bound to the permanent object location.

Now, elision isn't always possible given the ABI that a compiler compiles to. Two common cases where it is possible are known as Return Value Optimization and Named Return Value Optimization.

RVO is the case like this:

Foo func() {
  return Foo(7);
}
Foo foo = func();

where we have a return value Foo(7) which is elided into the value returned, which is then elided into the external variable foo. What appears to be 3 objects (the return value of foo(), the value on the return line, and Foo foo) is actually 1 at runtime.

Prior to the copy/move constructors must exist here, and the elision is optional; in due to the new prvalue rules no copy/move constructr need exist, and there is no option for the compiler, there must be 1 value here.

The other famous case is named return value optimization, NRVO. This is the (1) elision case above.

Foo func() {
  Foo local;
  return local;
}
Foo foo = func();

again, elision can merge the lifetime and identity of of Foo local, the return value from func and Foo foo outside of func.

Even , the second merge (between func's return value and Foo foo) is non-optional (and technically the prvalue returned from func is never an object, just an expression, which is then bound to construct Foo foo), but the first remains optional, and requires a move or copy constructor to exist.

Elision is a rule that can occur even if eliminating those copies, destructions and constructions would have observable side effects; it is not an "as-if" optimization. Instead, it is subtle change away from what a naive person might think C++ code means. Calling it an "optimization" is more than a bit of a misnomer.

The fact it is optional, and that subtle things can break it, is an issue with it.

Foo func(bool b) {
  Foo long_lived;
  long_lived.futz();
  if (b)
  {
    Foo short_lived;
    return short_lived;
  }
  return long_lived;
}

in the above case, while it is legal for a compiler to elide both Foo long_lived and Foo short_lived, implementation issues make it basically impossible, as both objects cannot both have their lifetimes merged with the return value of func; eliding short_lived and long_lived together is not legal, and their lifetimes overlap.

You can still do it under as-if, but only if you can examine and understand all side effects of destructors, constructors and .futz().

참고URL : https://stackoverflow.com/questions/52931095/exact-moment-of-return-in-a-c-function

반응형