본문으로 건너뛰기
Previous
Next
C++ return Statement — Complete Guide

C++ return Statement — Complete Guide

C++ return Statement — Complete Guide

이 글의 핵심

A practical guide to the C++ return statement: value and void returns, references, pointers, RVO, and safe patterns.

Introduction

The return statement ends a function and passes a value back to the caller. In C++, RVO (Return Value Optimization) lets the compiler eliminate unnecessary copies on return, which helps you write efficient code.


What you actually see at work

When you learn, everything feels neat and theoretical. Production is different: legacy code, tight deadlines, and bugs you did not expect. The topics here started as theory; they clicked once you apply them and think, “So that is why it is designed this way.”

What stuck from an early project was trial and error. I followed the book and still got stuck for days. A senior’s review surfaced the real issue, and I learned a lot from that. This article covers not only theory but also traps and fixes you are likely to hit in practice.

1. return basics

Returning a value

#include <iostream>

int add(int a, int b) {
    return a + b;  // return a value
}

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: " << sum << std::endl;           // 30
    std::cout << "division: " << result << std::endl;  // 3.33333
    
    return 0;
}

void return

#include <iostream>

void printMessage(const std::string& msg) {
    std::cout << msg << std::endl;
    return;  // optional at end of void function
}

void processData(int value) {
    if (value < 0) {
        std::cout << "Cannot process negative values" << std::endl;
        return;  // early exit
    }
    
    std::cout << "processing: " << value << std::endl;
}

int main() {
    printMessage("Hello");
    processData(-5);
    processData(10);
    
    return 0;
}

2. Return types

Returning by value

#include <string>
#include <vector>

// fundamental type
int getValue() {
    return 42;
}

// object return (RVO)
std::string getName() {
    return "Alice";
}

// container return (RVO)
std::vector<int> getNumbers() {
    return {1, 2, 3, 4, 5};
}

Returning references

#include <iostream>

class Array {
    int data[10] = {0};
    
public:
    // OK: reference to a member
    int& operator[](size_t index) {
        return data[index];
    }
    
    // OK: const reference
    const int& at(size_t index) const {
        return data[index];
    }
};

// OK: reference to a static
int& getGlobalCounter() {
    static int counter = 0;
    return counter;
}

int main() {
    Array arr;
    arr[0] = 42;  // modify via reference
    
    int& counter = getGlobalCounter();
    counter++;
    
    std::cout << arr[0] << ", " << counter << std::endl;
    
    return 0;
}

Returning pointers

#include <iostream>

// OK: return dynamically allocated memory
int* createInt(int value) {
    return new int(value);  // caller must delete
}

// OK: pointer to member storage
class Container {
    int data[10];
    
public:
    int* getData() {
        return data;
    }
};

// BAD: pointer to a local
int* getBad() {
    int x = 10;
    return &x;  // dangling pointer!
}

int main() {
    int* ptr = createInt(42);
    std::cout << *ptr << std::endl;
    delete ptr;  // manual cleanup
    
    return 0;
}

3. RVO (Return Value Optimization)

RVO basics

Example main and helper usage:

#include <iostream>
#include <string>

class Widget {
public:
    Widget() {
        std::cout << "Widget constructed" << std::endl;
    }
    
    Widget(const Widget&) {
        std::cout << "Widget copy" << std::endl;
    }
    
    Widget(Widget&&) noexcept {
        std::cout << "Widget move" << std::endl;
    }
};

// RVO: copy/move may be elided
Widget createWidget() {
    return Widget();  // construct in place
}

int main() {
    Widget w = createWidget();
    // prints: "Widget constructed" (no copy/move)
    
    return 0;
}

NRVO (Named RVO)

#include <string>
#include <iostream>

std::string createString() {
    std::string s = "Hello";  // local
    return s;  // NRVO may elide the copy
}

int main() {
    std::string str = createString();
    std::cout << str << std::endl;
    
    return 0;
}

Do not block RVO

#include <string>

// BAD: std::move can block NRVO
std::string bad() {
    std::string s = "Hello";
    return std::move(s);  // interferes with NRVO
}

// GOOD: plain return (RVO applies)
std::string good() {
    std::string s = "Hello";
    return s;  // NRVO applies
}

4. Multiple return values

std::pair

Example main usage:

#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: " << result << std::endl;
    } else {
        std::cout << "cannot divide by zero" << std::endl;
    }
    
    return 0;
}

std::tuple

#include <tuple>
#include <iostream>
#include <string>

std::tuple<int, std::string, double> getUserInfo() {
    return {25, "Hong Gildong", 175.5};
}

int main() {
    auto [age, name, height] = getUserInfo();
    
    std::cout << "name: " << name << std::endl;
    std::cout << "age: " << age << std::endl;
    std::cout << "height: " << 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 << "value: " << result.value() << std::endl;
    } else {
        std::cout << "parse failed" << std::endl;
    }
    
    // or
    int value = parseInt("123").value_or(0);
    
    return 0;
}

5. Common problems

Problem 1: returning a reference to a local

#include <iostream>

// BAD: dangling reference
const std::string& bad() {
    std::string s = "Hello";
    return s;  // s is destroyed when the function returns
}

// GOOD: return by value (RVO)
std::string good() {
    std::string s = "Hello";
    return s;  // safe
}

int main() {
    // const std::string& ref = bad();  // undefined behavior
    std::string str = good();  // safe
    
    std::cout << str << std::endl;
    
    return 0;
}

Problem 2: not returning on all paths

#include <iostream>

// BAD: missing return
int bad(bool flag) {
    if (flag) {
        return 10;
    }
    // missing return (typically a warning)
}

// GOOD: return on every path
int good(bool flag) {
    if (flag) {
        return 10;
    }
    return 0;  // else branch
}

int main() {
    std::cout << good(true) << std::endl;
    std::cout << good(false) << std::endl;
    
    return 0;
}

Problem 3: return type mismatch

Example for bad and related functions:

// BAD: type mismatch
int bad() {
    return "Hello";  // const char* -> int (warning/error)
}

// GOOD: correct type
std::string good() {
    return "Hello";
}

// GOOD: auto deduction
auto autoGood() {
    return "Hello";  // deduced as const char*
}

Problem 4: unnecessary std::move

#include <string>

// BAD: std::move blocks RVO
std::string bad() {
    std::string s = "Hello";
    return std::move(s);  // unnecessary
}

// GOOD: plain return
std::string good() {
    std::string s = "Hello";
    return s;  // RVO applies
}

6. Practical example: error handling

#include <optional>
#include <string>
#include <iostream>
#include <fstream>

class FileReader {
public:
    // explicit success/failure
    std::optional<std::string> readFile(const std::string& path) {
        std::ifstream file(path);
        
        if (!file.is_open()) {
            return std::nullopt;  // failure
        }
        
        std::string content;
        std::string line;
        
        while (std::getline(file, line)) {
            content += line + "\n";
        }
        
        return content;  // success
    }
    
    // errors via exception
    std::string readFileOrThrow(const std::string& path) {
        std::ifstream file(path);
        
        if (!file.is_open()) {
            throw std::runtime_error("failed to open file: " + 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 << "file contents:\n" << content.value() << std::endl;
    } else {
        std::cout << "failed to read file" << std::endl;
    }
    
    // exception
    try {
        std::string content2 = reader.readFileOrThrow("data.txt");
        std::cout << content2 << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "error: " << e.what() << std::endl;
    }
    
    return 0;
}

Summary

Key takeaways

  1. return: ends the function and returns a value.
  2. By value: copy or move (often optimized with RVO).
  3. By reference: safe for members or static storage only.
  4. Pointers: watch object lifetime and ownership.
  5. RVO: elision of return-value copies (stronger guarantees since C++17).
  6. Multiple values: std::pair, std::tuple, std::optional.

Choosing a return type

SituationPreferred returnWhy
Fundamental typesBy valuecheap to copy
ObjectsBy valueRVO applies
Mutate a memberReferencedirect modification
Possible failurestd::optionalclear failure channel
Several valuesstd::tuplestructured return
Rich error infoExceptionpropagate error details

Practical tips

Safety:

  • Return locals by value, not by reference.
  • Return on every path (enable -Wreturn-type or equivalent).
  • Document lifetime when returning raw pointers.

Performance:

  • Trust RVO; return by value.
  • Avoid std::move on return locals (can block NRVO).
  • Large objects are still usually fine by value with RVO.

Readability:

  • Use std::optional for clear failure.
  • Use std::tuple for multiple values.
  • Use C++17 structured bindings.

Next steps



자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. C++ return: basics, return types, references, pointers, RVO/NRVO, multiple return values (pair, tuple, optional), and co… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, return, function, RVO, return statement 등으로 검색하시면 이 글이 도움이 됩니다.