C++, 복사 생성자, 대입 연산자.
글쓴이: wafe / 작성시간: 월, 2004/03/01 - 4:50오후
MyClass라는 클래스를 정의했습니다. 이 클래스는 기본 생성자, 복사 생성자와 대입 연산자를 정의 합니다.
My Class { ... MyClass(); -> 기본 생성자 MyClass(const MyClass&); -> 복사 생성자 MyClass& operator=(const MyClass&); -> 대입 연산자 .. };
그리고 이 클래스를 값으로 리턴하는 함수가 있습니다.
MyClass func1() { MyClass ret; ... return ret; }
이렇게 정의해 두고, 기본 생성자, 복사 생성자, 대입 연산자에 출력문을 두고 다음과 같이 테스트 해 보았습니다.
MyClass a = func1();
이 경우에는 기본 생성자, 복사 생성자 순서로 호출됩니다. 그런데 다음과 같이 사용하면,
MyClass b; b = func1();
기본 생성자, 기본 생성자, 복사 생성자, 대입 연산자 순서로 호출됩니다.
여기서 두 번째의 경우에 func1() 안에서 ret를 선언할 때 기본 생성자가 호출되었고, return 할 때 임시 개체를 생성하여 ret를 복사하면서 복사 생성자가 호출되고, 임시 개체를 b에 대입할 때 대입 연산자가 호출되는 것으로 생각할 수 있습니다.
제 생각에는 중간에 임시 개체를 생성하는 것은 불필요할 것 같은데, 왜 임시 개체를 사용하는 것일까요? 반환값을 바로 b 에다가 바로 대입해 주는게 더 효율적이지 않나요?
Forums:
소멸자가 실행되는 위치가 문제가 될 것 같습니다.
기본적으로 자동지역객체는 함수가 종료되는 순간 사라져야 합니다.
이 의미를 그대로 따른다면 ret 는 함수를 호출한 쪽에서 사용할 수 없습니다.
ret 를 그대로 대입문에서 사용하고자 한다면 ret 의 소멸은 함수를 호출한 쪽에서 작업을 끝낼 때까지 지연되어야 합니다.
그렇게 하는 한가지 방법은 소멸자를 함수내에서 실행시키는 것이 아니라 함수호출한 쪽에서 하게 하는 방법입니다.
실제로 최적화에 대한 issue 중에 반환객체에 대한 이야기가 있는 것으로 압니다.
최적화정도에 따라 wafe 씨의 code 는 정확히 어떻게 객체들이 생성되고, 소멸될지는 알 수 없습니다.
잘은 모르겠습니다만 몇가지 calling convention 중 이런 최적화를 지원하는 방법이 있는 것으로 압니다.
음... 그렇다면 C++에서는 개체가 언제 생성되고 소멸될지 정확히 예측
음... 그렇다면 C++에서는 개체가 언제 생성되고 소멸될지 정확히 예측할 수는 없다는 것이군요.
아무튼, 제가 보여드린 코드에서 첫 번째 방법과 두 번째 방법의 동작 방식에 차이가 있는 것이 신기합니다.
제가 보여드린 첫 번째 예제
의 실행결과에서는 복사 생성자만 불립니다. 즉, 임시 개체가 생성되지 않고, func1()에서 선언된 ret를 b의 복사 생성자에 직접 전달해 준다고 생각할 수 있습니다. 그런데 두 번째 코드의 실행결과에서는 임시 개체가 생성되는 것으로 보인다고 앞에서도 말씀 드렸습니다.
첫 번째 예제에서 ret를 b의 복사 생성자에서 사용한다면, 함수가 종료된 뒤에도 자동지역개체가 남아있다는 것이므로 말이 되지 않습니다. 그렇다면 임시 개체를 만들어서 ret를 복사하고, 그냥 그 녀석을 b로 사용해버리는 것일까요? 이것도 그다지 신통한 설명은 아닌듯하네요.
Heejoon Lee
C++에서 객체가 언제 생성되고, 소멸되는지 알수 없다는 것은 말이 안되
C++에서 객체가 언제 생성되고, 소멸되는지 알수 없다는 것은 말이 안되는것 같습니다.
저의 짧은 생각으로 올리신 코드의 차이점은 '초기화'와 '대입'의 차이가 있는것 같습니다.
처음의 코드는 초기화 코드로 객체는 func()내에서 생성이 되고 이것이 카피가 되어 객체가 생성이 된 예이고, 두번째 예는 객체를 하나 생성한 후에 다시 함수를 호출하여 그 함수에서 생성된 객체를 카피하여 먼저 만든 객체에 값을 카피하여 할당하여 준거 같습니다.
즉, 말씀하신 임시객체라는 것은 복사생성자에 의해서 이루어 지는 일련의 작업(?)이라고 생각이 됩니다.
두서없이 쓰게 되었군요... ^^*
최적화 된 것 같습니다.
저도 착각을 했습니다만 wafe 씨의 글을 찬찬히 읽으니 초기와와 대입의 차이를 모르시는 것 같지는 않습니다.
아시는지 모르겠습니다만 static 이나 전역에서 선언되는 정적객체는 알 수 없습니다.
솔직히 저는 대충만 이해하고 있습니다만... -_-
첫번째 예제는 wafe 씨가 마지막에 적으신 것과 같은 개념으로 최적화되었다고 봅니다.
calling convention 에 의한 최적화는 아닌 것으로 보이네요.
이부분은 임시객체가 생성될지는 알 수 없다로 바꾸어야 할 것 같습니다. calling convention 의 변형은 예외... -_-
뭐가 어렵다는건지 모르겠군요...
C에서 파라미터와 리턴값은 call by value를 이용합니다. 이를 계승한 C++도 똑같은 방법을 이용합니다.
즉 리턴값의 복사본을 만들고 그 복사본을 넘겨 준다는겁니다. 두번째 예로 적으신 코드를 보겠습니다.
MyClass b; // 여기서 기본 생성자가 호출됩니다.
b = func1();
func1()을 자세히 보죠.
MyClass func1()
{
MyClass ret; // 여기서 또 기본 생성자
...
return ret; // 값을 리턴합니다. 복사본을 만들죠. 그래서 복사 생성자
}
그리고 이걸
b = func1 () ; // 즉 b에 대입합니다.
그래서 기본,기본,복사,대입 생성자가 실행이 됩니다. 여기서 예측이 안된다거나 문제가 되는게 어딘지요? calling convention에 의한 최적화라는게 뭔지도 모르겠구요.
ps. 사실 가장 큰 문제는 위의 func1 ()에 있습니다. 함수에서 클래스를 생성하고 그걸 리턴하는 구조도 이해를 못하겠군요. 저라면
와 같은 방법이나 func1 ()을 friend로 만들고 MyClass를 포인터나 참조로
넘겨주는 방법을 사용했을것 같네요.
산넘어 산
아 저런 코드를 쓴 것은 복사 생성자와 대입 연산자가 언제 불리는지 시험
아 저런 코드를 쓴 것은 복사 생성자와 대입 연산자가 언제 불리는지 시험해 보기 위한 것이었습니다. 제가 무엇을 궁금해 하는 것인지 설명이 부족했던것 같습니다.
MyClass a = func1();
에서는 복사 생성자가 한번만 불립니다(임시 반환개체 없이 a만 생성된다고 볼 수 있음. 혹은 임시 반환개체가 그대로 a로 바뀐다고 볼 수 있음).
MyClass b;
b = func1();
에서는 복사 생성자가 불리고 대입도 합니다(임시로 반환 개체가 생성되고 이것을 b에 대입한다고 생각할 수 있음).
즉, 첫 번째 코드와 두 번째 코드에서 일어나는 값에 의한 반환의 실제 행동이 다르게 보인다는 것입니다. 두 번째 코드에서 일어나는 일로 미루어 짐작컨대, 첫 번째 코드에서는 복사 생성자가 두 번 불리는 것이 정상이라고 생각됩니다. 즉,
MyClass a = func1();
이라는 코드에서는 func1()의 ret를 임시 반환 개체에 복사할 때 한 번, 임시 반환 개체를 a에 복사할 때 한 번 불릴 것이라고 예상할 수 있다는 것이지요. 그런데 실제로는 그렇지 않고 복사 생성자는 한 번만 호출됩니다. 이것이 가능하다면,
MyClass b;
b = func1();
이라는 코드에서도 "기본, 기본, 복사, 대입"의 순서가 아니라 "기본, 기본, 대입"만으로 실행되는 것이 가능할 것이라고 생각되고, 후자가 더 효율적이라고 생각됩니다.
지금으로서는 최적화 과정에서 일어나는 차이(임시 반환 개체가 생성되고 되지 않는 차이)라고 보는게 가장 설득력있네요. :)
Heejoon Lee
Re: 뭐가 어렵다는건지 모르겠군요...
...
wafe 님은 반환시에 일어나는 객체의 생성/소멸관계에 관심이 있으신것만
wafe 님은 반환시에 일어나는 객체의 생성/소멸관계에 관심이 있으신것만 봐도 본격적인 고민을 시작하셨다고 봐도 과언이 아닙니다. :)
생성될 때 대입되는 꼴로 만들어질때는 func1안에서 기본생성자가 그리고 대입되는 꼴에서는 복사생성자가 불리는 것이 쉽게 이해가 갑니다.
두번째 예에서는
func1에서의 기본생성자 대입전 기본생성자 까지는 두개가 이해가 됩니다만, 복사 생성자 없이 바로 함수 안에 있는 녀석이 대입될 수 없지 않겠느냐입니다.
문제는 그 대입연산자가 인자로 받는 (대개 const & 겠지요) 가시 범위가 func1까지 미치지 못한다는 데 있습니다. 따라서 한번의 복사생성이 일어나서 대입연산자의 인자로 넘길 수 있는 가시범위내에 복사생성되어야만 합니다.
이렇게 생각하면, 왜 처음 예에서는 복사생성 두번일어나지 않느냐고 반문할 수 있는데, 그 임시 생성이 필요한 곳이 다른 객체의 생성을 위한 복사생성일 경우에는 최적화되어 한번으로 멈추게 됩니다.
어렵습니다... :(
---
http://coolengineer.com
[code:1]MyClass func1() {
대신에
식으로 코드를 짜면 기본생성자를 부르는 걸 막을 수 있습니다.
예를 들어 complex 숫자 둘을 더하는 연산의 경우
식보다
식으로 부르는 것처럼요.
그런데 compiler 는 무엇을 쓰시나요?
백문이 불여일견이라고 한번 code 를 작성해보아습니다.
그런데 저는 임시객체를 생성하지 않는군요.
그냥 대입연산 한번합니다.
제가 test 한 환경은 Fedora Core 1 - kernel 2.6.0-1, GCC 3.3.2 입니다.
초기화와 이미 생성된 개체에 대입하는 차이 입니다.
MyClass a = func1 () ;
은 초기화와 물려 있기 때문입니다.
를 비교해보셔도 같은 결과가 나오죠. 초기화와 이미 생성되어 있는 개체는 다르게 처리됩니다. 이부분에 대한 정확한 스펙을 보지 않아서 장담할수는 없지만 초기화와 대입에 관련된 고전적 문제라고 알고 있습니다. 다른문제와는 관련이 없어 보이는데요.
ps. 위에 코드를 적고 보니 최초에 제기된 문제의 코드와 다를바가 없군요 -_-
산넘어 산
잘 모르지만 읽던 책에 있는 내용이라 적습니다...
RVO(Return Value Optimizaition)와 연관된 것 같군요.
MyClass func1()
{
MyClass ret;
...
return ret;
}
과
MyClass func1()
{
...
return MyClass(); <--- 이름없는 객체 생성
}
는 확실히 다른겁니다...
이름 있는 객체의 반환과 이름 없는 객체의 반환에서 이름없는쪽은
RVO가 수행되어 임시객체 생성이 따로 필요 없다고 하는군요.
( 사실 객체가 하나 밖에 없으니... ㅡ_ㅡ);
MyClass c = func1();
했더니 기본생성자,대입연산자 이렇게 호출되네요.
틀린곳 있으면 다른분이 꼭 알려주시길...
MEC++ 항목20에 자세히 나와있군요... :wink:
제 경우는 어떤 식으로 해도 대입연산만 수행했습니다.
과
모두 대입연산만 수행합니다.
그리고 기본생성자와 복사생성자를 단어선택(개념은 아시는 것 같은데...)을 실수하신 것 같네요.
위의 code 는 MyClass c( func1() ); 과 동일합니다.
위의 내용은 반환객체에 대한 내용이 아니라 func1() 내에서 객체생성을 할 때 기본생성자를 사용하지 않기 위한 내용입니까?
위의 소스 코드를 한번 분석해 보죠먼저 func1 은 다음과 같겠
위의 소스 코드를 한번 분석해 보죠
먼저 func1 은 다음과 같겠죠
제가 알고 있는게 맞다면 위와 같이 수행됩니다.
그럼 다음 코드를 분석해 보죠.
MyClass a = func1();
여기서 MyClass의 생성자는 explicit로 선언이 안되 있습니다.
그러므로 위의 코드는 다음과 같이 바뀝니다.
MyClass a(func1());
그러면 위의 func1이 불린후 a의 생성자 MyClass(const MyClass&);가 호출됩니다. 여기서 &가 붙었으므로 복사 생성자는 불리지 않습니다.
다음 코드를 보죠
제가 알고 있는게 맞으면 위와 같이 호출이 수행 됩니다.
아 그런데 실제 머신 코드가 생성될때는 최적화 기능들 때문에 다른 상황이 발생 할수 있습니다. 그러나 제대로 한다면 위와 같이 되야 합니다.
無心
댓글 달기