C++ Return Statement | "반환문" 가이드

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

정리

핵심 요약

  1. return: 함수 종료 및 값 반환
  2. 값 반환: 복사 또는 이동 (RVO로 최적화)
  3. 레퍼런스 반환: 멤버/static 변수만 안전
  4. 포인터 반환: 수명 관리 주의
  5. RVO: 반환값 복사 생략 (C++17 보장)
  6. 다중 반환: 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++ 함수 오버로딩 |