발단
C++을 통해 알고리즘 문제를 풀다가, 함수에서 std::string을 반환해도 되는지 의문이 생겼다.
지역 변수(local variable)로 선언된 변수는 함수를 빠져나오는 순간 사라진다. 그래서 함수에서 string과 같은 클래스를 반환하면 함수 밖에서는 이를 사용하지 못할 거라 생각했다.
그래서 기존에는 클래스, 하물며 구조체를 반환할 때에도 무조건 포인터를 전달해주었다.
#include <string>
void toString(string* dst)
{
dst = new string("abc");
}
나쁜 방법은 아니지만 함수 밖에서 포인터를 반드시 해제해주어야 한다는 점이 문제였다.
만약 delete로 힙 영역의 객체를 지우는 걸 깜빡한다면 메모리 누수까지 발생할 수 있었다.
그러나 <string> 헤더 파일의 to_string 함수는 string 형을 반환한다.
즉, 다음과 같이 반환된다는 것이다.
#include <string>
std::string toString()
{
std::string str = "abc";
return str;
}
함수는 객체를 반환할 때 복사된다
C++에서 객체는 함수에서 값으로 반환이 된다.
지역 변수라고 소멸되는 것이 아니라 복사 생성자를 통해 외부에 전달될 수 있다.
클래스 하나를 만들어서 직접 테스트해보자.
// TempClass.h
#pragma once
#include <iostream>
// 임시용 클래스
class TempClass
{
public:
TempClass() : mValue(0) { std::cout << "TempClass::TempClass()\n"; }
TempClass(const TempClass& temp)
{
std::cout << "TempClass::TempClass(const TempClass&)\n";
this->mValue = temp.mValue;
}
~TempClass() { std::cout << "TempClass::~TempClass()\n"; }
void set(int value) { mValue = value; }
int get() { return mValue; }
private:
int mValue;
};
TempClass.h에 TempClass를 만들었다.
TempClass는 매개 변수가 없는 생성자, 복사 생성자, 소멸자가 호출될 때 콘솔에 메시지를 출력한다.
또한 set, get을 통해 값을 간단하게 출력할 수 있게 만들었다.
// main.cpp
#include <iostream>
#include "TempClass.h"
TempClass Generate(int val) // 함수에서 객체를 값으로 반환한다.
{
TempClass local;
local.set(val);
return local;
}
int main()
{
TempClass temp = Generate(1024);
std::cout << temp.get() << '\n';
return 0;
}
main.cpp에서는 Generate 함수에서 지역변수를 값으로 반환하도록 하고 있다.
Generate에서 반환된 객체는 바깥의 temp 변수에 저장된다.
정상적으로 객체가 반환되었는지 확인하기 위해 get() 함수로 지정된 mValue를 출력한다.
이 코드를 실행한 결과는 다음과 같다.
// Output:
TempClass::TempClass()
TempClass::TempClass(const TempClass&)
TempClass::~TempClass()
1024
TempClass::~TempClass()
위 코드는 실행 환경에서 다르게 나올 수 있다.
Visual Studio 2019에서는 위와 같이 생성자와 복사 생성자가 호출되고, 소멸자가 두 번 호출된다.
Generate 함수가 반환되면, 함수 내부의 TempClass local 변수에서 main 클래스의 temp 클래스로 객체가 복사된다. 이때 복사 생성자가 호출되는 것으로 보인다.
정리하면 다음과 같다.
- 지역 변수라고 해서 소멸되지 않는다.
- 복사 생성자를 통해 외부의 temp 변수에 전달이 된다.
- Output을 보면 TempClass::TempClass(const TempClass&)가 호출되는 것을 볼 수 있는데, 복사 생성자를 통해 값이 전달되는 것을 보여준다.
- 기본적으로 C++에서는 복사 생성자를 만들지 않으면 컴파일러에서 자동으로 얕은 복사 생성자를 만들어주므로, 객체 반환을 해도 별 문제가 생기지 않는다.
- 다만 얕은 복사가 이뤄지면서 메모리 문제가 발생할 수가 있다.
- 이 경우에는 깊은 복사(deep copy) 생성자를 만들어줘서 해결해야 한다.
string도 클래스이므로 함수에서 반환되면 깊은 복사(deep copy)로 내용을 복사해서 반환해준다.
좀 더 구체적으로 들어가면, C++는 지역변수로 된 객체를 반환할 시 임시 객체를 통해 반환하도록 한다.
- 임시 객체란?
- 사용자 정의 형식(ex: 클래스)을 반환하는 함수의 반환 값을 저장하려는 경우 생성된다.
- EX) ComplexResult = Complex1 + Complex2 + Complex3
- 따라서 객체가 소멸되더라도 함수가 임시 객체를 거쳐 객체를 반환하면, 함수 밖에서 정상적으로 복사할 수 있게 된다.
구조체 반환
공부를 하다가 구조체 또한 반환이 가능하다는 걸 알게 됐다.
C++이 아니라 C언어에서도 동일하다.
- 포인터를 사용해서 구조체를 반환받을 필요는 없다.
- 컴파일러는 암시적으로 구조체는 int와 같은 value처럼 복사해줄 것이다.
#include <stdio.h> struct a { int i; }; struct a f(struct a x) { struct a r = x; return r; } int main(void) { struct a x = { 12 }; struct a y = f(x); printf("%d\\n", y.i); return 0; }
- 따라서 함수에서 구조체를 반환하는 데에는 아무런 문제가 없다.
- 실질적으로 구조체를 반환하면 깊은 복사(deep copy)가 이루어진다.
- struct a x에 f(x)의 결과인 { 12 }가 복사된다.
- 그러나 이렇게 복사하는 것은 CPU 사이클을 낭비하는 셈이다.
- 구조체를 자주 반환하게 되면 stack에 구조체를 복사했다가 다시 날리고 또 복사했다가 다시 날리고를 반복하게 된다.
- 너무 구조체가 클 경우엔 매개변수로 전달하거나 함수에서 반환하는 게 비효율적일 수 있다.
- 이때에는 포인터로 구조체를 전달하는 편이 낫다.
다만 구조체의 반환은 컴파일러나 플랫폼에 따라 내부에서 어떻게 구현되는지는 달라질 수 있다고 한다.
- 만약 RVO(Return value optimization)를 구현하였다면 구조체나 클래스의 복사가 굉장히 빨라질 수 있다.
객체가 복사되면 느리지 않을까?
그런데 string이 반환되면서 객체 전체가 깊은 복사가 되니까 포인터나 참조 변수를 통해 값을 전달하는 것보다 더 느리지 않냐는 지적도 있다.
- 함수를 반환할 때 string의 객체 전체를 깊은 복사하는 과정이 느릴 것처럼 보인다.
- 그러나 최신 컴파일러에서는 RVO(Return value optimization)을 지원하여 포인터 인자로 간접적으로 넘기는 것만큼 최적화를 해준다고 한다.
다만 RVO와 임시 객체는 자세히 공부해보지 않아 추가적으로 공부해볼 필요가 있다. 따라서 다소 신뢰도가 낮을 수 있다.
참고 링크
Returning a Class Object | Efficient C++ Programming | InformIT
'프로그래밍 언어 > C, C++' 카테고리의 다른 글
[C++] 레퍼런스, const 레퍼런스 반환 / 레퍼런스와 임시 개체의 수명 (1) | 2024.02.05 |
---|---|
[C/C++] 메모리 초기화하기: ZeroMemory 매크로 (0) | 2023.08.06 |
[C/C++] 함수 호출 규약 __stdcall과 __cdecl에 관하여 (0) | 2023.08.03 |
[C++] 스마트 포인터 - unique_ptr과 안전하게 쓰는 방법 (0) | 2023.07.20 |
C언어에서의 16진수, signed int와 unsigned int (0) | 2021.05.03 |
댓글