C++ Return Statement | "반환문" 가이드
이 글의 핵심
C++ Return Statement에 대한 실전 가이드입니다.
들어가며
return문은 함수 실행을 종료하고 값을 호출자에게 반환합니다. C++에서는 RVO(Return Value Optimization)를 통해 반환 시 불필요한 복사를 제거하여 효율적인 코드를 작성할 수 있습니다.
1. return 기본
값 반환
#include <iostream>
int add(int a, int b) {
return a + b; // 값 반환
}
double divide(int a, int b) {
return static_cast<double>(a) / b;
}
int main() {
int sum = add(10, 20);
double result = divide(10, 3);
std::cout << "합: " << sum << std::endl; // 30
std::cout << "나눗셈: " << result << std::endl; // 3.33333
return 0;
}
void 반환
#include <iostream>
void printMessage(const std::string& msg) {
std::cout << msg << std::endl;
return; // 생략 가능
}
void processData(int value) {
if (value < 0) {
std::cout << "음수는 처리할 수 없습니다" << std::endl;
return; // 조기 종료
}
std::cout << "처리: " << value << std::endl;
}
int main() {
printMessage("Hello");
processData(-5);
processData(10);
return 0;
}
2. 반환 타입
값 반환
#include <string>
#include <vector>
// 기본 타입
int getValue() {
return 42;
}
// 객체 반환 (RVO)
std::string getName() {
return "Alice";
}
// 컨테이너 반환 (RVO)
std::vector<int> getNumbers() {
return {1, 2, 3, 4, 5};
}
레퍼런스 반환
#include <iostream>
class Array {
int data[10] = {0};
public:
// ✅ 멤버 변수 레퍼런스 반환
int& operator {
return data[index];
}
// ✅ const 레퍼런스 반환
const int& at(size_t index) const {
return data[index];
}
};
// ✅ static 변수 레퍼런스 반환
int& getGlobalCounter() {
static int counter = 0;
return counter;
}
int main() {
Array arr;
arr[0] = 42; // 레퍼런스로 수정
int& counter = getGlobalCounter();
counter++;
std::cout << arr[0] << ", " << counter << std::endl;
return 0;
}
포인터 반환
#include <iostream>
// ✅ 동적 할당 반환
int* createInt(int value) {
return new int(value); // 호출자가 delete 책임
}
// ✅ 멤버 포인터 반환
class Container {
int data[10];
public:
int* getData() {
return data;
}
};
// ❌ 지역 변수 포인터 반환
int* getBad() {
int x = 10;
return &x; // 댕글링 포인터!
}
int main() {
int* ptr = createInt(42);
std::cout << *ptr << std::endl;
delete ptr; // 수동 삭제 필요
return 0;
}
3. RVO (Return Value Optimization)
RVO 기본
#include <iostream>
#include <string>
class Widget {
public:
Widget() {
std::cout << "Widget 생성" << std::endl;
}
Widget(const Widget&) {
std::cout << "Widget 복사" << std::endl;
}
Widget(Widget&&) noexcept {
std::cout << "Widget 이동" << std::endl;
}
};
// RVO: 복사/이동 생략
Widget createWidget() {
return Widget(); // 직접 생성
}
int main() {
Widget w = createWidget();
// 출력: "Widget 생성" (복사/이동 없음)
return 0;
}
NRVO (Named RVO)
#include <string>
#include <iostream>
std::string createString() {
std::string s = "Hello"; // 지역 변수
return s; // NRVO (복사 생략 가능)
}
int main() {
std::string str = createString();
std::cout << str << std::endl;
return 0;
}
RVO 방해하지 않기
#include <string>
// ❌ std::move로 RVO 방해
std::string bad() {
std::string s = "Hello";
return std::move(s); // NRVO 방해!
}
// ✅ 그냥 반환 (RVO 적용)
std::string good() {
std::string s = "Hello";
return s; // NRVO 적용
}
4. 다중 반환값
std::pair
#include <utility>
#include <iostream>
std::pair<bool, int> divide(int a, int b) {
if (b == 0) {
return {false, 0};
}
return {true, a / b};
}
int main() {
auto [success, result] = divide(10, 2);
if (success) {
std::cout << "결과: " << result << std::endl;
} else {
std::cout << "0으로 나눌 수 없음" << std::endl;
}
return 0;
}
std::tuple
#include <tuple>
#include <iostream>
#include <string>
std::tuple<int, std::string, double> getUserInfo() {
return {25, "홍길동", 175.5};
}
int main() {
auto [age, name, height] = getUserInfo();
std::cout << "이름: " << name << std::endl;
std::cout << "나이: " << age << std::endl;
std::cout << "키: " << height << std::endl;
return 0;
}
std::optional (C++17)
#include <optional>
#include <iostream>
#include <string>
std::optional<int> parseInt(const std::string& str) {
try {
return std::stoi(str);
} catch (...) {
return std::nullopt;
}
}
int main() {
auto result = parseInt("123");
if (result.has_value()) {
std::cout << "값: " << result.value() << std::endl;
} else {
std::cout << "파싱 실패" << std::endl;
}
// 또는
int value = parseInt("123").value_or(0);
return 0;
}
5. 자주 발생하는 문제
문제 1: 지역 변수 레퍼런스 반환
#include <iostream>
// ❌ 댕글링 레퍼런스
const std::string& bad() {
std::string s = "Hello";
return s; // s는 함수 종료 시 소멸!
}
// ✅ 값 반환 (RVO 적용)
std::string good() {
std::string s = "Hello";
return s; // 안전
}
int main() {
// const std::string& ref = bad(); // 정의되지 않은 동작!
std::string str = good(); // 안전
std::cout << str << std::endl;
return 0;
}
문제 2: 모든 경로에서 반환
#include <iostream>
// ❌ 반환 누락
int bad(bool flag) {
if (flag) {
return 10;
}
// 반환 누락! (경고)
}
// ✅ 모든 경로에서 반환
int good(bool flag) {
if (flag) {
return 10;
}
return 0; // else 경로
}
int main() {
std::cout << good(true) << std::endl;
std::cout << good(false) << std::endl;
return 0;
}
문제 3: 반환 타입 불일치
// ❌ 타입 불일치
int bad() {
return "Hello"; // const char* -> int (경고)
}
// ✅ 올바른 타입
std::string good() {
return "Hello";
}
// ✅ auto 타입 추론
auto autoGood() {
return "Hello"; // const char* 추론
}
문제 4: 불필요한 std::move
#include <string>
// ❌ std::move로 RVO 방해
std::string bad() {
std::string s = "Hello";
return std::move(s); // 불필요!
}
// ✅ 그냥 반환
std::string good() {
std::string s = "Hello";
return s; // RVO 적용
}
6. 실전 예제: 에러 처리
#include <optional>
#include <string>
#include <iostream>
#include <fstream>
class FileReader {
public:
// 성공/실패를 명확히 반환
std::optional<std::string> readFile(const std::string& path) {
std::ifstream file(path);
if (!file.is_open()) {
return std::nullopt; // 실패
}
std::string content;
std::string line;
while (std::getline(file, line)) {
content += line + "\n";
}
return content; // 성공
}
// 예외로 에러 처리
std::string readFileOrThrow(const std::string& path) {
std::ifstream file(path);
if (!file.is_open()) {
throw std::runtime_error("파일 열기 실패: " + path);
}
std::string content;
std::string line;
while (std::getline(file, line)) {
content += line + "\n";
}
return content;
}
};
int main() {
FileReader reader;
// optional 사용
auto content = reader.readFile("data.txt");
if (content.has_value()) {
std::cout << "파일 내용:\n" << content.value() << std::endl;
} else {
std::cout << "파일 읽기 실패" << std::endl;
}
// 예외 사용
try {
std::string content2 = reader.readFileOrThrow("data.txt");
std::cout << content2 << std::endl;
} catch (const std::exception& e) {
std::cerr << "에러: " << e.what() << std::endl;
}
return 0;
}
정리
핵심 요약
- return: 함수 종료 및 값 반환
- 값 반환: 복사 또는 이동 (RVO로 최적화)
- 레퍼런스 반환: 멤버/static 변수만 안전
- 포인터 반환: 수명 관리 주의
- RVO: 반환값 복사 생략 (C++17 보장)
- 다중 반환:
std::pair,std::tuple,std::optional
반환 타입 선택 가이드
| 상황 | 권장 반환 타입 | 이유 |
|---|---|---|
| 기본 타입 | 값 | 복사 비용 낮음 |
| 객체 | 값 | RVO 적용 |
| 멤버 변수 수정 | 레퍼런스 | 직접 수정 가능 |
| 실패 가능 | std::optional | 명확한 실패 표현 |
| 여러 값 | std::tuple | 구조화된 반환 |
| 에러 상세 | 예외 | 에러 정보 전달 |
실전 팁
안전성:
- 지역 변수는 값으로 반환 (레퍼런스 금지)
- 모든 경로에서 반환 (
-Wreturn-type경고 확인) - 포인터 반환 시 수명 문서화
성능:
- RVO를 신뢰하고 값으로 반환
std::move사용 자제 (RVO 방해)- 큰 객체도 값 반환 (RVO 적용)
가독성:
std::optional로 실패 명확히 표현std::tuple로 여러 값 반환- C++17 Structured Binding 활용
다음 단계
- C++ RVO/NRVO
- C++ Copy Elision
- C++ Move Semantics
관련 글
- C++ std::function vs 함수 포인터 |
- C++ 기본 인자 |
- C++ RVO·NRVO |
- C++ 함수 |
- C++ 함수 오버로딩 |