#22. [모던 C++] 임시 구체화와 복사 생략 보증(C++17)
- (C++17~) 임시 구체화와 복사 생략 보증을 통해 컴파일러 의존적이었던 생성자 호출 및 함수 인수 전달 최적화, 리턴값 최적화등이 표준화 되었습니다.
개요
기존에는 다양한 상황에서 생성된 임시 개체를 컴파일러가 자체적으로 최적화 하여 불필요한 복사 생성이나 복사 대입이 최소화 되도록 해줬습니다.
-
임시 개체를 그냥 임시 개체를 저장할 변수로 사용합니다.(생성자 호출 및 함수 인수 전달 최적화 와 중괄호 복사 초기화 참고)
-
리턴할 개체를 그냥 리턴값을 저장할 변수로 사용합니다.
컴파일러 종류에 따라 다르겠지만, GCC에서는 상기 최적화를 수행하여 불필요한 이동 생성을 생략하고, 생성된 임시 개체를 그냥 사용합니다.
1
2
3
4
5
6
7
8
class A {};
A a{A{}}; // A{}를 기본 생성자로 생성하고 a에 이동 생성 합니다. 하지만 컴파일러에 따라 생성된 임시 개체를 그냥 a로 사용합니다.
void Func1(A param) {}
Func1(A{}); // A{}를 기본 생성자로 생성하고, param에 이동 생성 합니다. 하지만 컴파일러에 따라 전달된 임시 개체를 그냥 param으로 사용합니다.
A Func2() {return A{};}
A result = Func2(); // A{} 기본 생성자로 생성하고, result에 이동 생성합니다. 하지만 컴파일러에 따라 리턴된 임시 개체를 그냥 result로 사용합니다.
아쉬운 점은, 컴파일러 최적화에 따라 이동 생성자를 사용하지 않더라도, 문법적으로는 이동 생성자가 필요하므로, 이동 생성자를 delete하면 컴파일 오류가 난다는 점입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
class A_11 {
public:
int m_Val;
public:
A_11() = default;
A_11(const A_11& other) = delete;
A_11(A_11&& other) noexcept = delete; // 이동 생성자 사용 안함
A_11& operator =(const A_11& other) = delete;
A_11& operator =(A_11&& other) noexcept = delete;
};
// (X) ~C++17 컴파일 오류. 이동 생성자가 없습니다.
A_11 a{A_11{}};
그래서 억지로 다음처럼 이동 생성자를 사용했었습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
class A_11 {
public:
int m_Val;
public:
A_11() = default;
A_11(const A_11& other) = delete;
A_11(A_11&& other) noexcept = default; // 억지로 이동 생성자를 사용함
A_11& operator =(const A_11& other) = delete;
A_11& operator =(A_11&& other) noexcept = delete;
};
// 컴파일러 최적화로 이동 생성자를 사용하지 않지만, 컴파일을 위해 문법적으로는 이동 생성자가 필요합니다.
A_11 a{A_11{}};
하지만, C++17 부터는 임시 구체화와 복사 생략 보증을 통해 임시 개체가 불필요하게 복사나 이동되지 않음을 문법적으로 보증해 줍니다. 그덕에 상기 경우에 이동 생성자를 억지로 사용하지 않아도 컴파일 오류없이 잘 동작합니다.
임시 구체화(Temporary materialization)
C++17 부터 임시 개체는 다른 개체를 초기화 하는데에만 사용되며, 특별히 다음의 경우에도 임시적으로 구체화되는 것으로 한정했습니다.
-
임시 개체를 const형 참조자로 바인딩할때(임시 개체 참고)
1 2 3 4
class T {}; // T& a = T{}; // (X) 컴파일 오류. 임시 개체를 T&로 참조할 수 없습니다. const T& b = T{}; // (O) T{}는 임시 구체화 됩니다.
-
임시 개체 멤버에 접근할때
1 2 3 4 5 6
class T { public: int m_Val; }; int val = T{}.m_Val; // T{} 는 임시 구체화 됩니다.
-
임시 배열을 포인터로 변환할때
1 2 3 4
const int* ptr = (const int[]){1, 2, 3}; // 임시 배열을 const int* 로 변환해서 저장합니다. 임시 배열은 임시 구체화 됩니다. EXPECT_TRUE(*(ptr + 0) == 1); EXPECT_TRUE(*(ptr + 1) == 2); EXPECT_TRUE(*(ptr + 2) == 3);
-
임시 배열의 첨자 연산(
[]
)을 할때1 2
int val = (int[]){1, 2, 3}[1]; // 임시 배열의 [1]요소에 접근합니다. 임시 배열은 임시 구체화 됩니다. EXPECT_TRUE(val == 2);
-
중괄호 초기화에서 initializer_list를 초기화할때(initializer_list의 암시적 생성 참고)
1 2 3 4 5
class T_11 { public: explicit T_11(std::initializer_list<int>) {} }; T_11 t({1, 2, 3}); // {1, 2, 3} 은 initializer_list로 임시 구체화됩니다.
복사 생략(Copy elision)
C++17 부터 임시 개체는 임시 구체화 기준에 따라 최대한 구체화를 하지 않으며, 최종 대상의 메모리 위치에 직접 구현됩니다.
1
2
3
4
5
6
7
8
9
A a{A{}}; // A{}는 a에 구현됩니다.
void Func1(A param) {}
Func1(A{}); // A{}는 param에 구현됩니다.
A Func2() {
return A{}; // A{}는 result에 구현됩니다. return move(A{}); 하지 마세요.
}
A result = Func2(); // A{}는 result에 구현됩니다. move(Func2()) 하지 마세요.
즉, 다음 코드에서 A_17 a{A_17{}};
는 임시 개체인 A_17{}
가 a
에 직접 구현되므로, 문법적으로 이동 생성을 사용하지 않습니다. 따라서 C++17 에서는 다음과 같이 이동 생성자를 delete 해도 정상적으로 컴파일됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A_17 {
public:
int m_Val;
public:
A_17() = default;
A_17(const A_17& other) = delete; // 복사 생성자 사용 안함
A_17(A_17&& other) noexcept = delete; // 이동 생성자 사용 안함
A_17& operator =(const A_17& other) = delete; // 복사 대입 연산자 사용 안함
A_17& operator =(A_17&& other) noexcept = delete; // 이동 대입 연산자 사용 안함
};
// (O) 문법적으로 복사 생성자와 이동 생성자를 사용하지 않습니다.
// 임시 개체인 A_17{}을 그냥 a 변수로 사용합니다.
A_17 a{A_17{}};
댓글남기기