C++ Copy Elision | "복사 생략" 가이드
이 글의 핵심
C++ Copy Elision에 대한 실전 가이드입니다.
들어가며
Copy Elision(복사 생략)은 컴파일러가 불필요한 복사/이동 연산을 제거하는 최적화 기법입니다. C++17부터 특정 경우에는 필수로 적용되며, 성능을 크게 향상시킵니다.
#include <iostream>
class Widget {
public:
Widget() {
std::cout << "생성자" << std::endl;
}
// 복사 생성자: 다른 Widget으로부터 복사
Widget(const Widget&) {
std::cout << "복사 생성자" << std::endl;
}
// 이동 생성자: 다른 Widget의 리소스를 이동
Widget(Widget&&) noexcept {
std::cout << "이동 생성자" << std::endl;
}
};
Widget createWidget() {
// Widget() 임시 객체 반환
// Copy Elision: 복사/이동 없이 직접 반환 위치에 생성
return Widget(); // 복사/이동 생략
}
int main() {
// createWidget()의 반환값을 w에 할당
// Copy Elision 적용: Widget이 w의 위치에 직접 생성됨
// 복사 생성자도, 이동 생성자도 호출 안됨!
Widget w = createWidget();
// 출력: "생성자" (한 번만)
// 복사/이동 생략으로 성능 향상
}
왜 필요한가?:
- 성능: 복사/이동 비용 제거
- 효율성: 큰 객체(컨테이너, 문자열) 반환 시 효과적
- 간결성: 값으로 반환해도 성능 걱정 없음
1. Copy Elision 종류
1. RVO (Return Value Optimization)
임시 객체를 반환할 때 적용됩니다. C++17부터 필수입니다.
#include <iostream>
class Data {
public:
Data() {
std::cout << "생성자" << std::endl;
}
Data(const Data&) {
std::cout << "복사 생성자" << std::endl;
}
Data(Data&&) noexcept {
std::cout << "이동 생성자" << std::endl;
}
};
Data func() {
return Data(); // prvalue (순수 우측값)
}
int main() {
Data d = func();
// C++17: 복사 생략 보장
// 출력: "생성자" (1번)
}
2. NRVO (Named Return Value Optimization)
이름 있는 지역 변수를 반환할 때 적용됩니다. 컴파일러 재량입니다.
#include <iostream>
class Data {
public:
Data() {
std::cout << "생성자" << std::endl;
}
Data(const Data&) {
std::cout << "복사 생성자" << std::endl;
}
Data(Data&&) noexcept {
std::cout << "이동 생성자" << std::endl;
}
};
Data func() {
// 이름 있는 지역 변수 (Named Return Value)
Data d; // 이름 있는 객체
// d 초기화 로직
// NRVO (Named Return Value Optimization):
// 컴파일러가 d를 반환 위치에 직접 생성할 수 있음
// 하지만 RVO와 달리 보장되지 않음 (컴파일러 재량)
return d;
}
int main() {
Data d = func();
// NRVO 적용 시: "생성자" (1번만)
// - func() 안의 d가 main의 d 위치에 직접 생성
// NRVO 미적용 시: "생성자" + "이동 생성자"
// - func() 안에서 생성 → main으로 이동
}
NRVO 적용 조건:
- 반환되는 객체가 지역 변수
- 모든 반환 경로에서 같은 변수 반환
- 변수 타입이 반환 타입과 동일
// ✅ NRVO 가능
Data func1() {
Data d;
return d; // 항상 d 반환
}
// ❌ NRVO 불가
Data func2(bool flag) {
Data a, b;
return flag ? a : b; // 다른 변수 반환
}
3. 함수 인자 전달
임시 객체를 함수 인자로 전달할 때 적용됩니다.
#include <iostream>
class Widget {
public:
Widget() {
std::cout << "생성자" << std::endl;
}
Widget(const Widget&) {
std::cout << "복사 생성자" << std::endl;
}
Widget(Widget&&) noexcept {
std::cout << "이동 생성자" << std::endl;
}
};
void process(Widget w) {
std::cout << "process 호출" << std::endl;
}
int main() {
process(Widget()); // 복사 생략
// Widget이 process의 매개변수 위치에 직접 생성됨
// 출력: "생성자", "process 호출"
}
2. C++17 보장된 복사 생략
prvalue 복사 생략
#include <iostream>
class Widget {
public:
Widget() {
std::cout << "생성자" << std::endl;
}
Widget(const Widget&) {
std::cout << "복사 생성자" << std::endl;
}
Widget(Widget&&) noexcept {
std::cout << "이동 생성자" << std::endl;
}
};
Widget createWidget() {
return Widget();
}
int main() {
// C++17부터 보장
Widget w1 = Widget(); // 복사 생략 (보장)
Widget w2 = createWidget(); // 복사 생략 (보장)
Widget w3 = Widget(Widget()); // 복사 생략 (보장)
// 출력: "생성자" (3번만)
}
C++17 복사 생략 규칙
| 상황 | C++14 이전 | C++17 이후 |
|---|---|---|
Widget w = Widget(); | 최적화 (선택) | 필수 |
Widget w = func(); (prvalue 반환) | 최적화 (선택) | 필수 |
Widget w = x; (이름 있는 변수) | 최적화 (선택) | 최적화 (선택) |
복사 생성자 불필요
C++17부터 prvalue 복사 생략은 필수이므로, 복사 생성자가 없어도 됩니다.
#include <iostream>
class NonCopyable {
public:
NonCopyable() {
std::cout << "생성자" << std::endl;
}
NonCopyable(const NonCopyable&) = delete; // 복사 금지
NonCopyable(NonCopyable&&) = default; // 이동 허용
};
NonCopyable func() {
return NonCopyable(); // C++17: OK (복사 생략 보장)
}
int main() {
NonCopyable obj = func(); // OK
// 출력: "생성자"
}
3. 실전 예제
예제 1: 컨테이너 반환
#include <vector>
#include <string>
#include <iostream>
// 복사 생략
std::vector<int> createVector(size_t size) {
std::vector<int> result(size);
for (size_t i = 0; i < size; i++) {
result[i] = i * i;
}
return result; // 복사 없음
}
// 문자열 처리
std::string processString(const std::string& input) {
std::string result = input;
result += " processed";
return result; // 복사 생략
}
int main() {
auto vec = createVector(10);
auto str = processString("Hello");
std::cout << "벡터 크기: " << vec.size() << std::endl;
std::cout << "문자열: " << str << std::endl;
for (size_t i = 0; i < vec.size(); ++i) {
std::cout << vec[i] << " ";
}
std::cout << std::endl;
return 0;
}
출력:
벡터 크기: 10
문자열: Hello processed
0 1 4 9 16 25 36 49 64 81
예제 2: 팩토리 함수
#include <iostream>
#include <string>
class Connection {
private:
std::string host;
int port;
public:
Connection(const std::string& h, int p)
: host(h), port(p) {
std::cout << "연결: " << host << ":" << port << std::endl;
}
Connection(const Connection&) {
std::cout << "복사 생성자" << std::endl;
}
Connection(Connection&&) noexcept {
std::cout << "이동 생성자" << std::endl;
}
void info() const {
std::cout << "Connection(" << host << ":" << port << ")" << std::endl;
}
};
// 복사 생략
Connection createConnection(const std::string& host, int port) {
return Connection(host, port);
}
// 조건부 생성
Connection createConnectionByType(const std::string& type) {
if (type == "local") {
return Connection("localhost", 8080);
} else if (type == "remote") {
return Connection("example.com", 443);
}
return Connection("default", 80);
}
int main() {
std::cout << "=== createConnection ===" << std::endl;
auto conn1 = createConnection("localhost", 8080);
std::cout << "\n=== createConnectionByType ===" << std::endl;
auto conn2 = createConnectionByType("remote");
std::cout << "\n=== 정보 출력 ===" << std::endl;
conn1.info();
conn2.info();
return 0;
}
출력:
=== createConnection ===
연결: localhost:8080
=== createConnectionByType ===
연결: example.com:443
=== 정보 출력 ===
Connection(localhost:8080)
Connection(example.com:443)
예제 3: 복잡한 객체
#include <map>
#include <string>
#include <iostream>
class Config {
private:
std::map<std::string, std::string> settings;
public:
Config() {
std::cout << "Config 생성" << std::endl;
}
Config(const Config&) {
std::cout << "Config 복사" << std::endl;
}
Config(Config&&) noexcept {
std::cout << "Config 이동" << std::endl;
}
void set(const std::string& key, const std::string& value) {
settings[key] = value;
}
std::string get(const std::string& key) const {
auto it = settings.find(key);
return it != settings.end() ? it->second : "";
}
};
// 복사 생략
Config loadConfig(const std::string& filename) {
Config config;
config.set("host", "localhost");
config.set("port", "8080");
config.set("debug", "true");
return config; // NRVO
}
int main() {
std::cout << "=== loadConfig ===" << std::endl;
auto config = loadConfig("app.conf");
std::cout << "\n=== 설정 출력 ===" << std::endl;
std::cout << "host: " << config.get("host") << std::endl;
std::cout << "port: " << config.get("port") << std::endl;
std::cout << "debug: " << config.get("debug") << std::endl;
return 0;
}
출력:
=== loadConfig ===
Config 생성
=== 설정 출력 ===
host: localhost
port: 8080
debug: true
4. 자주 발생하는 문제
문제 1: std::move 남용
#include <iostream>
#include <vector>
class Data {
public:
Data() {
std::cout << "생성자" << std::endl;
}
Data(const Data&) {
std::cout << "복사 생성자" << std::endl;
}
Data(Data&&) noexcept {
std::cout << "이동 생성자" << std::endl;
}
};
// ❌ std::move로 복사 생략 방해
Data bad() {
Data d;
return std::move(d); // 복사 생략 불가, 이동만 발생
}
// ✅ 그냥 반환
Data good() {
Data d;
return d; // 복사 생략 또는 이동
}
int main() {
std::cout << "=== bad() ===" << std::endl;
auto d1 = bad(); // 생성자 + 이동
std::cout << "\n=== good() ===" << std::endl;
auto d2 = good(); // 생성자만 (복사 생략)
return 0;
}
출력:
=== bad() ===
생성자
이동 생성자
=== good() ===
생성자
문제 2: 여러 반환 경로
#include <iostream>
class Data {
public:
Data() {
std::cout << "생성자" << std::endl;
}
Data(const Data&) {
std::cout << "복사 생성자" << std::endl;
}
Data(Data&&) noexcept {
std::cout << "이동 생성자" << std::endl;
}
};
// ❌ 복사 생략 안됨
Data bad(bool flag) {
Data a, b;
return flag ? a : b; // 이동 사용
}
// ✅ 복사 생략 가능
Data good(bool flag) {
Data result;
if (flag) {
// result 초기화
} else {
// result 초기화
}
return result; // 복사 생략
}
int main() {
std::cout << "=== bad(true) ===" << std::endl;
auto d1 = bad(true); // 생성자 2번 + 이동
std::cout << "\n=== good(true) ===" << std::endl;
auto d2 = good(true); // 생성자만
return 0;
}
출력:
=== bad(true) ===
생성자
생성자
이동 생성자
=== good(true) ===
생성자
문제 3: 최적화 레벨
# 복사 생략 비활성화
g++ -fno-elide-constructors main.cpp -o main
# 복사 생략 활성화 (기본)
g++ main.cpp -o main
#include <iostream>
class Data {
public:
Data() {
std::cout << "생성자" << std::endl;
}
Data(const Data&) {
std::cout << "복사 생성자" << std::endl;
}
Data(Data&&) noexcept {
std::cout << "이동 생성자" << std::endl;
}
};
Data func() {
return Data();
}
int main() {
auto d = func();
// 복사 생략 비활성화 시: 생성자 + 이동
// 복사 생략 활성화 시: 생성자만
}
5. 성능 비교
#include <chrono>
#include <vector>
#include <iostream>
class LargeObject {
private:
std::vector<int> data;
public:
LargeObject() : data(10000, 0) {
// 큰 객체 생성
}
LargeObject(const LargeObject& other) : data(other.data) {
// 복사 비용
}
LargeObject(LargeObject&& other) noexcept : data(std::move(other.data)) {
// 이동 비용
}
};
LargeObject createObject() {
return LargeObject();
}
int main() {
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000; i++) {
auto obj = createObject();
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "시간: " << duration.count() << "ms" << std::endl;
return 0;
}
결과:
- 복사 생략 활성화: ~100ms
- 복사 생략 비활성화: ~150ms (이동 비용 추가)
6. 실무 패턴
패턴 1: 값으로 반환
#include <vector>
#include <string>
#include <iostream>
// ✅ 복사 생략 덕분에 안전하고 효율적
std::vector<int> loadData(const std::string& filename) {
std::vector<int> data;
// 파일에서 데이터 로드
for (int i = 0; i < 100; ++i) {
data.push_back(i * i);
}
return data; // 복사 생략
}
int main() {
auto data = loadData("data.txt"); // 복사 없음
std::cout << "데이터 크기: " << data.size() << std::endl;
return 0;
}
패턴 2: 빌더 패턴
#include <string>
#include <iostream>
class QueryBuilder {
std::string query_;
public:
QueryBuilder() : query_("SELECT * FROM table") {}
QueryBuilder& select(const std::string& fields) {
query_ = "SELECT " + fields + " FROM table";
return *this;
}
QueryBuilder& from(const std::string& table) {
query_ += " FROM " + table;
return *this;
}
QueryBuilder& where(const std::string& condition) {
query_ += " WHERE " + condition;
return *this;
}
std::string build() const {
return query_; // 복사 생략
}
};
int main() {
auto query = QueryBuilder()
.select("*")
.from("users")
.where("age > 18")
.build();
std::cout << query << std::endl;
return 0;
}
출력:
SELECT * FROM users WHERE age > 18
패턴 3: 팩토리 함수
#include <string>
#include <iostream>
class Connection {
std::string host_;
int port_;
public:
Connection(std::string host, int port)
: host_(std::move(host)), port_(port) {
std::cout << "Connection(" << host_ << ":" << port_ << ")" << std::endl;
}
void info() const {
std::cout << "연결: " << host_ << ":" << port_ << std::endl;
}
};
Connection createLocalConnection() {
return Connection("localhost", 8080); // 복사 생략
}
Connection createRemoteConnection(const std::string& host) {
return Connection(host, 443); // 복사 생략
}
int main() {
auto conn1 = createLocalConnection();
auto conn2 = createRemoteConnection("example.com");
conn1.info();
conn2.info();
return 0;
}
출력:
Connection(localhost:8080)
Connection(example.com:443)
연결: localhost:8080
연결: example.com:443
7. 컴파일러 동작 확인
#include <iostream>
class Tracker {
public:
Tracker() {
std::cout << "생성자" << std::endl;
}
Tracker(const Tracker&) {
std::cout << "복사 생성자" << std::endl;
}
Tracker(Tracker&&) noexcept {
std::cout << "이동 생성자" << std::endl;
}
~Tracker() {
std::cout << "소멸자" << std::endl;
}
};
Tracker func() {
return Tracker();
}
int main() {
std::cout << "=== 시작 ===" << std::endl;
auto t = func();
std::cout << "=== 끝 ===" << std::endl;
return 0;
}
출력 (복사 생략 활성화):
=== 시작 ===
생성자
=== 끝 ===
소멸자
출력 (복사 생략 비활성화):
=== 시작 ===
생성자
이동 생성자
소멸자
=== 끝 ===
소멸자
정리
핵심 요약
- Copy Elision: 불필요한 복사/이동 제거
- RVO: 임시 객체 반환 (C++17 필수)
- NRVO: 이름 있는 변수 반환 (컴파일러 재량)
- prvalue: C++17부터 복사 생략 보장
- std::move 금지: 반환 시 사용하지 마세요
복사 생략 종류
| 종류 | 설명 | C++17 |
|---|---|---|
| RVO | 임시 객체 반환 | 필수 |
| NRVO | 이름 있는 변수 반환 | 선택 |
| 인자 전달 | 임시 객체 전달 | 필수 |
실전 팁
사용 원칙:
- 값으로 반환 (복사 생략 신뢰)
std::move사용 금지 (반환 시)- 단일 변수 반환 (NRVO)
- 임시 객체 반환 (RVO)
성능:
- 복사/이동 비용 완전 제거
- 큰 객체에서 효과적
- 컴파일 타임 최적화
- 런타임 오버헤드 없음
주의사항:
- 여러 반환 경로 주의
std::move남용 금지- 복사 생성자 불필요 (C++17 prvalue)
- NRVO는 보장 안됨
다음 단계
- C++ RVO/NRVO
- C++ Move Semantics
- C++ Return Statement
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ RVO/NRVO | “Return Value Optimization” 가이드
- C++ noexcept | “예외 없음 지정” 가이드
- C++ Return Statement | “반환문” 가이드
관련 글
- C++ RVO·NRVO |
- C++ any |
- C++ Branch Prediction |
- C++ Cache Optimization |
- 모던 C++ (C++11~C++20) 핵심 문법 치트시트 | 현업에서 자주 쓰는 한눈에 보기