C++ Temporary Objects | "임시 객체" 가이드
이 글의 핵심
임시 객체(temporary objects) 는 표현식 평가 중에 생성되는 이름 없는 객체입니다. 일반적으로 표현식이 끝나면 즉시 소멸되지만, const 레퍼런스나 우측값 레퍼런스로 바인딩하면 수명이 연장됩니다.
임시 객체란?
임시 객체(temporary objects) 는 표현식 평가 중에 생성되는 이름 없는 객체입니다. 일반적으로 표현식이 끝나면 즉시 소멸되지만, const 레퍼런스나 우측값 레퍼런스로 바인딩하면 수명이 연장됩니다.
std::string s = std::string("Hello"); // 임시 객체 생성
// std::string("Hello")는 임시 객체
int x = 1 + 2; // 3은 임시 값
왜 중요한가?:
- 성능: 불필요한 임시 객체는 성능 저하
- 안전성: 임시 객체의 댕글링 레퍼런스 방지
- 최적화: RVO, NRVO, 이동 의미론 이해
임시 객체 vs 일반 객체:
| 특징 | 일반 객체 | 임시 객체 |
|---|---|---|
| 이름 | ✅ 있음 | ❌ 없음 |
| 수명 | 스코프 끝 | 표현식 끝 |
| 수명 연장 | - | ✅ const 레퍼런스 |
| 이동 가능 | ✅ | ✅ |
| 복사 가능 | ✅ | ✅ |
// 일반 객체
std::string name("Alice"); // 스코프 끝까지 유효
// 임시 객체
std::string("Alice"); // 표현식 끝에 소멸
임시 객체 생성 시점
// 1. 함수 반환
std::string getName() {
return "Alice"; // 임시 객체
}
// 2. 타입 변환
void func(std::string s) {}
func("Hello"); // const char* -> std::string 임시 객체
// 3. 연산자
std::string s = "Hello" + std::string(" World"); // 임시 객체
// 4. 명시적 생성
Widget(10); // 임시 객체
임시 객체 수명
class Temp {
public:
Temp(int val) : value(val) {
std::cout << "생성: " << value << std::endl;
}
~Temp() {
std::cout << "소멸: " << value << std::endl;
}
private:
int value;
};
void func() {
Temp(10); // 즉시 소멸
std::cout << "다음 줄" << std::endl;
}
임시 객체 수명 규칙:
- 기본 규칙: 임시 객체는 전체 표현식(full expression) 이 끝나면 소멸됩니다.
#include <iostream>
class Temp {
public:
Temp(int v) : value(v) {
std::cout << "생성 " << value << '\n';
}
~Temp() {
std::cout << "소멸 " << value << '\n';
}
int get() const { return value; }
private:
int value;
};
int main() {
std::cout << "=== 시작 ===\n";
int x = Temp(10).get(); // 표현식 끝에 소멸
std::cout << "=== 끝 ===\n";
}
// 출력:
// === 시작 ===
// 생성 10
// 소멸 10
// === 끝 ===
- 수명 연장: const 레퍼런스 또는 우측값 레퍼런스로 바인딩하면 수명이 연장됩니다.
int main() {
std::cout << "=== 시작 ===\n";
const Temp& ref = Temp(10); // 수명 연장
std::cout << "중간\n";
std::cout << ref.get() << '\n';
std::cout << "=== 끝 ===\n";
}
// 출력:
// === 시작 ===
// 생성 10
// 중간
// 10
// === 끝 ===
// 소멸 10
- 수명 연장 예외: 함수 반환값의 멤버는 수명 연장되지 않습니다.
struct Inner {
int value = 42;
};
struct Outer {
Inner inner;
};
Outer getOuter() {
return Outer();
}
int main() {
// ❌ 댕글링 레퍼런스
const Inner& inner = getOuter().inner;
// getOuter()의 임시 객체는 즉시 소멸
// inner는 댕글링
// ✅ 전체 객체 저장
const Outer& outer = getOuter();
const Inner& inner2 = outer.inner; // 안전
}
실전 예시
예시 1: 수명 연장
#include <string>
std::string getName() {
return "Alice";
}
int main() {
// ❌ 즉시 소멸
const char* ptr = getName().c_str();
// getName()의 임시 객체 소멸
// ptr은 댕글링
// ✅ 수명 연장
const std::string& name = getName();
const char* ptr2 = name.c_str(); // 안전
// name이 스코프 벗어날 때까지 유효
}
예시 2: 함수 인자
#include <iostream>
class Widget {
public:
Widget(int val) : value(val) {
std::cout << "Widget 생성: " << value << std::endl;
}
~Widget() {
std::cout << "Widget 소멸: " << value << std::endl;
}
int getValue() const {
return value;
}
private:
int value;
};
void process(const Widget& w) {
std::cout << "처리: " << w.getValue() << std::endl;
}
int main() {
process(Widget(10)); // 임시 객체
// process 호출 후 소멸
}
예시 3: 반환값 최적화
#include <vector>
std::vector<int> createVector(size_t size) {
std::vector<int> result(size);
for (size_t i = 0; i < size; i++) {
result[i] = i;
}
return result; // 임시 객체 (RVO)
}
int main() {
auto vec = createVector(1000);
// RVO로 복사 없음
}
예시 4: 연산자 오버로딩
class Vector2 {
private:
float x, y;
public:
Vector2(float x, float y) : x(x), y(y) {}
Vector2 operator+(const Vector2& other) const {
return Vector2(x + other.x, y + other.y); // 임시 객체
}
Vector2 operator*(float scalar) const {
return Vector2(x * scalar, y * scalar); // 임시 객체
}
};
int main() {
Vector2 v1(1, 2);
Vector2 v2(3, 4);
Vector2 v3 = v1 + v2; // 임시 객체 생성
Vector2 v4 = v1 * 2.0f; // 임시 객체 생성
}
임시 객체 최적화
// RVO (Return Value Optimization)
std::string func1() {
return std::string("Hello"); // 임시 객체 생략
}
// NRVO (Named RVO)
std::string func2() {
std::string result = "Hello";
return result; // 복사 생략
}
// 이동 의미론
std::string func3() {
std::string result = "Hello";
return result; // 이동 (복사 생략 안될 때)
}
자주 발생하는 문제
문제 1: 댕글링 레퍼런스
// ❌ 임시 객체 즉시 소멸
std::string getName() {
return "Alice";
}
const char* ptr = getName().c_str();
// getName()의 임시 객체 소멸
// ptr은 댕글링
// ✅ 수명 연장
const std::string& name = getName();
const char* ptr = name.c_str();
문제 2: 비const 레퍼런스
// ❌ 임시 객체는 비const 레퍼런스 불가
void func(std::string& s) {}
// func("Hello"); // 에러
// ✅ const 레퍼런스 또는 우측값 레퍼런스
void func1(const std::string& s) {}
void func2(std::string&& s) {}
func1("Hello"); // OK
func2("Hello"); // OK
문제 3: 멤버 접근
class Data {
public:
int getValue() const {
return value;
}
private:
int value = 42;
};
Data getData() {
return Data();
}
int main() {
// ✅ 임시 객체 멤버 접근
int x = getData().getValue(); // OK
// ❌ 포인터 저장
// const Data* ptr = &getData(); // 에러
}
문제 4: 컨테이너 임시 객체
#include <vector>
std::vector<int> getVector() {
return {1, 2, 3};
}
int main() {
// ❌ 반복자 저장
// auto it = getVector().begin(); // 위험
// ✅ 컨테이너 저장
auto vec = getVector();
auto it = vec.begin(); // 안전
}
임시 객체 감지
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 getTracker() {
return Tracker();
}
int main() {
std::cout << "=== 시작 ===" << std::endl;
auto t = getTracker();
std::cout << "=== 끝 ===" << std::endl;
}
성능 고려사항
// ❌ 불필요한 임시 객체
std::string s = "Hello";
s = s + " World"; // 임시 객체 생성
// ✅ 직접 수정
std::string s = "Hello";
s += " World"; // 임시 객체 없음
// ❌ 반복문에서 임시 객체
for (int i = 0; i < 1000; i++) {
std::string s = std::string("Hello"); // 매번 생성
}
// ✅ 재사용
std::string s;
for (int i = 0; i < 1000; i++) {
s = "Hello";
}
임시 객체 비용 분석:
#include <chrono>
#include <iostream>
#include <string>
class LargeObject {
std::string data_;
public:
LargeObject(const std::string& data) : data_(data) {}
LargeObject(const LargeObject& other) : data_(other.data_) {
// 복사 비용
}
LargeObject(LargeObject&& other) noexcept : data_(std::move(other.data_)) {
// 이동 비용 (작음)
}
};
// ❌ 임시 객체 많이 생성
LargeObject process1(const LargeObject& obj) {
LargeObject result = obj; // 복사
// 처리
return result; // 이동 또는 복사 생략
}
// ✅ 이동 의미론 활용
LargeObject process2(LargeObject obj) { // 이동으로 받기
// 처리
return obj; // 이동 또는 복사 생략
}
// ✅ 직접 수정
void process3(LargeObject& obj) { // 레퍼런스
// obj 직접 수정
}
최적화 팁:
| 상황 | 비권장 | 권장 |
|---|---|---|
| 문자열 연결 | s = s + " World"; | s += " World"; |
| 컨테이너 추가 | v = v + element; | v.push_back(element); |
| 반복 생성 | for(...) { T obj; } | T obj; for(...) { obj = ...; } |
| 함수 반환 | return std::move(local); | return local; |
실무 패턴
패턴 1: 체이닝
class StringBuilder {
std::string buffer_;
public:
StringBuilder& append(const std::string& str) {
buffer_ += str;
return *this; // 임시 객체 없음
}
std::string build() const {
return buffer_; // 복사 생략
}
};
// 사용
auto result = StringBuilder()
.append("Hello")
.append(" ")
.append("World")
.build();
패턴 2: 팩토리 함수
class Connection {
std::string host_;
int port_;
public:
Connection(std::string host, int port)
: host_(std::move(host)), port_(port) {}
};
// 임시 객체 반환 (복사 생략)
Connection createConnection(const std::string& type) {
if (type == "local") {
return Connection("localhost", 8080);
}
return Connection("remote.example.com", 443);
}
패턴 3: 이동 의미론
class Resource {
std::vector<int> data_;
public:
Resource(size_t size) : data_(size) {}
// 이동 생성자
Resource(Resource&& other) noexcept
: data_(std::move(other.data_)) {}
};
// 임시 객체를 이동으로 받기
void process(Resource&& res) {
Resource local = std::move(res); // 이동
}
int main() {
process(Resource(1000)); // 임시 객체 이동
}
FAQ
Q1: 임시 객체는 언제 생성되나요?
A:
- 함수 반환 시
- 타입 변환 시
- 연산자 사용 시
- 명시적 생성 시 (예:
Widget(10))
Q2: 임시 객체의 수명은?
A:
- 기본: 전체 표현식이 끝나면 소멸
- 연장: const 레퍼런스 또는 우측값 레퍼런스로 바인딩 시 수명 연장
Q3: 성능 영향은?
A:
- RVO/NRVO로 대부분 최적화됨
- 이동 의미론으로 복사 비용 감소
- 불필요한 임시 객체는 성능 저하
Q4: 댕글링 레퍼런스를 방지하려면?
A:
- const 레퍼런스로 수명 연장
- 값으로 저장
- 임시 객체의 멤버는 저장하지 않기
Q5: 임시 객체를 최적화하는 방법은?
A:
+=등 직접 수정 연산자 사용- 불필요한 타입 변환 피하기
- 이동 의미론 활용
- RVO/NRVO 활용 (반환 시
std::move사용 안 함)
Q6: 임시 객체를 비const 레퍼런스로 받을 수 있나요?
A: 불가능합니다. 임시 객체는 const 레퍼런스 또는 우측값 레퍼런스로만 바인딩할 수 있습니다.
void func1(std::string& s) {} // 비const 레퍼런스
void func2(const std::string& s) {} // const 레퍼런스
void func3(std::string&& s) {} // 우측값 레퍼런스
// func1("Hello"); // 에러
func2("Hello"); // OK
func3("Hello"); // OK
Q7: 임시 객체 학습 리소스는?
A:
- “Effective C++” by Scott Meyers (Item 19)
- “C++ Primer” by Lippman, Lajoie, Moo
- cppreference.com - Lifetime
관련 글: Copy Elision, RVO/NRVO, Move Semantics.
한 줄 요약: 임시 객체는 표현식 평가 중 생성되는 이름 없는 객체로, 표현식 끝에 소멸되지만 const 레퍼런스로 수명을 연장할 수 있습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Dangling Reference | “댕글링 레퍼런스” 가이드
- C++ Lifetime | “객체 수명” 가이드
- C++ Copy Elision | “복사 생략” 가이드
관련 글
- C++ 시리즈 전체 보기
- C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
- C++ ADL |
- C++ Aggregate Initialization |