Linux g++에서 class copy constructor이 호출되지 않습니다...ㅠㅠ

jic5760의 이미지

소스는.. 아주 간단합니다.

class Test {
public:
	Test()
	{
		std::cout << "Test Create" << std::endl;
	}
	~Test()
	{
		std::cout << "Test Destroy" << std::endl;
	}
 
	Test(const Test& ref)
	{
		std::cout << "Test Copy 1" << std::endl;
	}
 
	Test& operator=(const Test& ref)
	{
		std::cout << "Test Copy 2" << std::endl;
		return *this;
	}
};
 
Test testfunc1()
{
	Test a;
	return a;
}
 
int main()
{
	Test a = testfunc1();
 
	return 0;
}

컴파일 환경은 G++ 4.8.5 입니다.
옵티마이저 옵션도 -O0 을 줘서 껐습니다..

그런데 왜 Test Copy 가 출력되지 않을까요???

Windows Visual C++ 에서는 잘 됩니다...

* 출력 :
Test Create
끝...

만약

Test a;
a = testfunc1();

을 하면
Test Create
Test Create
Test Copy 2
Test Destroy
이렇게 잘 됩니다...
 의 이미지

제가 학부 OOP 수업 실습 하다가 발견하고 경악을 금치 못했던 문제로군요.

답을 달아 드리고 싶은데 오늘 제가 일이 너무 많고 피곤해서 어렵겠습니다. 내일까지 다른 분이 답 안 주시면 제가 쓰도록 하죠.

jic5760의 이미지

아래에 다른 분께서 답변해주셨네요~ 감사합니다~

 의 이미지

저도 학부 때 OOP 들으면서 복사 생성자 함수 호출을 추적하다가 발견한 문제였죠.

일반적으로 컴파일러 최적화는 사용자 정의 함수 호출을 제거할 수 없습니다.
이러한 함수 호출은 Side effect가 있을 수 있기 때문이고, 그걸 함부로 제거하면 프로그램 동작이 크게 바뀔 수 있으니까요. (파일 입출력, 네트워크 송수신, DB 기록 등)

질문자님이 작성하신 복사 생성자의 동작, 즉 콘솔에 메시지를 출력하는 것 역시도 원래는 생략되어선 안 될 Side effect의 한 예입니다.

함수에서 값을 반환할 때는 분명히 클래스 객체가 복사됩니다. (C++03 std 12.8.1) 이 경우 복사 생성자(Copy constructor) 혹은 대입 연산자(Copy assignment operator)가 호출되죠. 이 두 함수는 사용자가 정의할 수 있으므로 위에서 언급한 일반론이 적용됩니다. 즉 프로그래머는 C++ 컴파일러가 이러한 함수 호출을 (최적화 조건에 상관 없이) 보존해 줄 것이라고 기대하게 됩니다.

... 그리고 C++ 표준은 이러한 기대를 통렬하게 배반합니다. 이 맛에 C++ 씁니다!

C++03 std 12.8.15 wrote:
When certain criteria are met, an implementation is allowed to omit the copy construction of a class object, even if the copy constructor and/or destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy 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. This elision of copy operations 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 with the same cv-unqualified type as the function return type, the copy operation can be omitted by constructing the automatic object directly into the function’s return value
— when a temporary class object that has not been bound to a reference (12.2) would be copied to a class object with the same cv-unqualified type, the copy operation can be omitted by constructing the temporary object directly into the target of the omitted copy

번역은 솔직히 별로 자신 없습니다만 할 수 있는 데까지 해 보죠.

C++03 std 12.8.15 wrote:
특정 조건이 만족되었을 경우, 구현환경은 설령 복사 생성자나 소멸자가 Side effect를 가지더라도 복사 생성자의 호출을 생략할 수 있다. 이 경우, 구현환경은 생략할 복사 동작의 원본과 복사본을 동일한 객체를 가리키는 것으로 간주하며, 그 객체의 소멸은 원래의 코드에서 원본과 복사본이 모두 소멸될 시점에서 일어난다. (역주: 의역했습니다.) 이러한 복사 동작의 생략은 아래의 상황에서 허용된다. (이러한 상황이 중첩될 경우 여러 차례 생략될 수 있다.)

- 클래스 반환 타입을 가지는 함수의 반환문에서, 반환할 표현식이 비휘발(역주: volatile이 아닌) 자동(역주: automatic, 쉽게 말해 static이 아닌 지역 변수) 객체의 이름이며 그 cv-unqualified type(역주: 짧게 설명하기 어렵네요. 타입에서 바깥의 const와 volatile을 뗀 것, 정도)이 함수 반환 타입의 cv-unqualified type과 일치하는 경우, 해당 자동 변수를 함수의 반환값 위치에 직접 씀으로써 복사를 생략할 수 있다.

- reference에 bound되지 않는 임시 클래스 객체가... 아놔 -_-;; 대강 번역한다고 모두 알아들으실 것도 아니고 배경설명까지 다 하려면 끝이 없겠네요. 여기까지면 충분하니 그냥 생략합니다.

네. 귀찮아서 번역을 하다 말았는데 중요한 부분은 다 나왔죠. 질문자님이 올리신 코드가 첫째 경우에 정확히 해당되므로, 컴파일러는 복사 생성자 및 소멸자를 날려버리고 함수 testfunc1의 자동 변수 a를 함수 main에서 반환값을 받아 오는 변수인 a 자리에 그냥 만들어버리는 겁니다.

C++03 표준에서 이 부분에 대한 예제 코드도 실어 놨는데 질문자님 코드와 비슷하네요.

class Thing {
public:
    Thing();
    ~Thing();
    Thing(const Thing&);
};
 
Thing f() {
    Thing t;
    return t;
}
 
Thing t2 = f();

어떻게 보면 이후 C++11에 도입된 이동 의미론(move semantics) 중에서도 특수한 경우라고 볼 수도 있겠네요. 곧 복제된 뒤 소멸될 것이 분명한 객체를 아예 복사본이 위치할 자리에 먼저 만들어 버림으로써, 복사 생성자 하나와 소멸자 하나의 비용을 절약한 것입니다. 어쨌든 생성자 한 번에 소멸자 한 번이 대응되는 불변식은 변함이 없습니다.

이런 최적화를 도입한 이유야 뭐 분명합니다. C++ 짜다 보면 함수 매개변수로 객체를 전달할 때는 선택의 여지가 좀 있지만, 값을 반환해야만 하는 상황에서는 다른 수가 없는 경우가 종종 있잖아요. 예컨대 operator+int의 그것과 비슷한 의미로 오버로딩하는 경우, 함수 안에서 새 값을 만들어서 반환해야 하는데 이건 포인터로도 안되고 참조자로도 안되죠. 이 경우 이러한 최적화를 통해 그나마 복사 생성자와 소멸자 한 쌍 값을 절약할 수 있죠. 위안이 되지 않습니까.

이 최적화는 C++03 표준에서는 이름을 지어 주지 않았지만, C++11 표준에서도 분명히 살아남아서 이제 이름까지 지어졌습니다. "copy elision"이라고 하더군요. 물론 더 오래 전부터 통용되던 명칭인 RVO(Return Value Optimization, 반환값 최적화)도 계속 통용될겁니다. 아무튼, C++11 표준에서는 이게 허용되는 상황이 2개에서 4개로 늘었는데, 번역하기 귀찮으니 관심 있으시면 찾아가서 보세요.(C++11 std 12.8.31) 짧게 언급하자면, 추가된 두 가지 상황은 예외 처리와 관련된 것입니다.

이러한 최적화는 "허용되는" 것이지 "강제되는" 것이 아니므로, 정말로 할지 말지는 컴파일러 개발사 마음입니다. g++처럼 좋은 컴파일러는 옵션으로도 제공하는 모양이지만요. 그런데 제가 존경하는, Effective C++의 저자 Scott Meyers님은 이렇게 말씀하시더군요. (More Effective C++, 곽용재 편역, 163p)

Scott Meyers wrote:
"에이구..." 투덜이 독자가 어디선가 궁시렁대고 있군요. "칫, 최적화. 말은 좋지. 빌어먹을 최적화. 컴파일러에서 그런 기능이 지원되는지 누가 아남? 이게 요즘 컴파일러에서 진짜 되긴 하는 거야?" 분명히 됩니다. 이 특수한 최적화(함수의 반환 위치를 써서 지역 임시 객체를 없애 주는, 그리고 경우에 따라 그 객체를 함수의 호출 위치에 있는 객체와 바꾸어 주는) 기능은 꽤 많이 알려진 것으로서 웬만한 컴파일러에 구현되어 있습니다. 이름까지 있습니다. 반환값 최적화(return value optimization : RVO)라고요. C++ 개발 도구 제작사에 전화를 걸어서, 그 회사의 컴파일러는 반환값 최적화가 지원되냐고 물어보세요 어떤 회사는 그런 것을 물어보는 여러분이 이상하다는 듯이 "예"라고 답할 것이고, "예?"라고 묻는 회사도 있을 것입니다. 전자는 후자보다 망할 확률이 낮을 것입니다. 자본주의란 것은 뭐 그런거죠 여러분도 좋아하게 될 것입니다.

하지만 그렇다고는 해도, 복사 생성자와 소멸자 안에 Side effect가 일어나게 작성할 경우 컴파일러가 최적화 여부에 따라 프로그램의 동작이 크게 바뀔 수 있다는 점을 염두에 두기는 해야겠죠.

twinwings의 이미지

RVO 및 생성자 관련된 최적화 때문입니다. 이 부분은 컴파일러에 의존합니다.

$ g++ --version
g++ (Ubuntu 5.4.0-6ubuntu1~16.04.2) 5.4.0 20160609
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

without -fno-elide-constructors option

$ g++ test.cpp
$ ./a.out 
Test Create
Test Destroy
$ 

with -fno-elide-constructors option

$ g++ test.cpp  -fno-elide-constructors
$ ./a.out 
Test Create
Test Copy 1
Test Destroy
Test Copy 1
Test Destroy
Test Destroy
$ 

http://stackoverflow.com/questions/8758152/disabling-gs-return-value-optimisation

jic5760의 이미지

옵티마이저만 끈다고 해결되는게 아니었네요..ㅠㅠ
정말 감사드립니다!
복사생성자 테스트해야할 일이 있었는데 덕분에 해결되었네요~^^

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.