C++ Copy Elision | "복사 생략" 가이드

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;
}

출력 (복사 생략 활성화):

=== 시작 ===
생성자
=== 끝 ===
소멸자

출력 (복사 생략 비활성화):

=== 시작 ===
생성자
이동 생성자
소멸자
=== 끝 ===
소멸자

정리

핵심 요약

  1. Copy Elision: 불필요한 복사/이동 제거
  2. RVO: 임시 객체 반환 (C++17 필수)
  3. NRVO: 이름 있는 변수 반환 (컴파일러 재량)
  4. prvalue: C++17부터 복사 생략 보장
  5. 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) 핵심 문법 치트시트 | 현업에서 자주 쓰는 한눈에 보기