`return` 문(return statement)

return 문(return statement)

함수가 할 일을 끝내고, 결과 값(있을 경우)을 호출한 쪽으로 돌려줘야 할 때 쓰는 문이 return이에요. return현재 함수를 종료하고 지정된 값을 호출자에게 반환해요.

출처: cppreference

본문

문법

attr(optional) return expression(optional) ; (1)
attr(optional) return braced-init-list ; (2) (C++11부터)
attr(optional) co_return expression(optional) ; (3) (C++20부터)
attr(optional) co_return braced-init-list ; (4) (C++20부터)

동작 방식(Explanation)

  1. 표현식을 평가하고, 반환 타입으로의 암시적 변환(implicit conversion)을 거친 다음, 현재 함수를 종료하고 그 결과를 호출자에게 반환해요. 반환 타입이 (cv 한정될 수 있는) void인 함수에서는 표현식을 생략할 수 있고, 생성자와 소멸자에서는 표현식을 쓸 수 없어요.

3,4) 코루틴(coroutine)에서는 마지막 중단 지점에서 return 대신 키워드 co_return을 써야 해요.

표현식 또는 braced-init-list(C++11부터)(있을 경우)를 return 문의 피연산자(operand)라고 불러요.

함수 호출 결과의 복사 초기화와 표현식 끝에서의 모든 임시 객체 파괴 사이에 시퀀스 포인트(sequence point)가 있다. (C++11까지)
함수 호출 결과의 복사 초기화는 표현식 끝에서의 모든 임시 객체 파괴보다 앞서 서열화되고(sequenced-before), 이는 다시 return 문을 둘러싼 블록의 지역 변수 파괴보다 앞서 서열화된다. (C++11부터)
함수의 반환 타입이 참조 타입이고 return 문 (1,2)이 반환된 참조를 임시 표현식의 결과에 바인딩하면, 프로그램은 ill-formed다. (C++26부터)

제어 흐름이 return 문을 만나지 않고 다음 함수의 끝에 도달하면 return;이 실행돼요.

  • 반환 타입이 (cv 한정될 수 있는) void인 함수
  • 생성자
  • 소멸자
  • 반환 타입이 (cv 한정될 수 있는) void인 함수의 함수 try 블록

제어 흐름이 main 함수의 끝에 도달하면 return 0;이 실행돼요.

값을 반환하는 함수에서 main 함수와 일부 코루틴(C++20부터)을 제외하고, return 문 없이 끝까지 흘러내려가는 것은 미정의 동작(undefined behavior)이에요.

(또는 cv 한정될 수 있는) void를 반환하는 함수에서, 표현식의 타입이 (또는 cv 한정될 수 있는) void라면 표현식을 가진 return 문을 쓸 수 있어요.

함수의 반환 타입이 자리 표시자 타입(placeholder type)으로 지정되면, 반환 값으로부터 타입이 추론(deduce)돼요. decltype(auto)를 쓰면 타입 추론이 엔티티가 될 수 있는 표현식을 엔티티로 취급해요. (C++14부터)

주의할 점(Notes)

값을 반환하면 복사 생략(copy elision)을 쓰지 않는 한 임시 객체의 생성과 복사/이동이 수반될 수 있어요. 복사/이동의 조건은 구체적으로 다음과 같아요.

지역 변수와 매개변수의 자동 이동(Automatic move)

표현식이 (괄호로 묶을 수 있는) 식별자 표현식(identifier expression)으로서, 자동 저장 기간(duration)을 가진 변수를 이름 짓고 그 타입이 - 비휘발성 객체 타입, 또는 - 비휘발성 객체 타입에 대한 rvalue 참조(C++20부터)이면서, 그 변수가 가장 안쪽 둘러싸는 함수나 람다의 본문 또는 매개변수로 선언되어 있으면, 그 표현식은 이동 자격(move-eligible)이 있다. (C++11부터)
표현식이 이동 자격이 있으면, 반환 값의 초기화에 쓸 생성자를 선택하거나(co_return의 경우 promise.return_value() 오버로드를 선택, C++20부터) 오버로드 해석(overload resolution)이 두 번 수행돼요. 첫 번째는 표현식이 rvalue인 것처럼(C++11부터이므로 이동 생성자를 선택할 수 있음), 그리고 첫 번째 오버로드 해석이 실패하면, 또는 성공해도 이동 생성자를 선택하지 않았으면, 표현식을 lvalue로 간주하고 평소처럼 오버로드 해석이 수행돼요(그래서 복사 생성자를 선택할 수 있음). (C++11부터)(C++23까지)
표현식이 이동 자격이 있으면, 그것은 xvalue로 취급돼요(그래서 오버로드 해석이 이동 생성자를 선택할 수 있음). (C++23부터)

보장된 복사 생략(Guaranteed copy elision)

표현식이 prvalue이면, 결과 객체는 그 표현식에 의해 직접 초기화돼요. 타입이 일치하면 복사나 이동 생성자가 관여하지 않아요. (C++17부터)
기능 테스트 매크로 표준 기능
__cpp_implicit_move 202207L (C++23) 더 단순해진 암시적 이동(Simpler implicit move)

예제

return의 여러 형태와, 이동 자격 있는 표현식이 어떻게 동작하는지 확인해 볼게요.

#include <iostream>
#include <string>
#include <utility>

void fa(int i)
{
    if (i == 2)
        return;
    std::cout << "fa(" << i << ")\n";
} // 암시적인 return;

int fb(int i)
{
    if (i > 4)
        return 4;
    std::cout << "fb(" << i << ")\n";
    return 2;
}

std::pair<std::string, int> fc(const char* p, int x)
{
    return {p, x};
}

void fd()
{
    return fa(10); // fa(10)은 void 표현식
}

int main()
{
    fa(1); // 인자를 출력하고 반환
    fa(2); // i == 2일 때 아무것도 하지 않고 그냥 반환

    int i = fb(5); // 4를 반환
    i = fb(i);     // 인자를 출력하고 2를 반환

    std::cout << "i = " << i << '\n'
              << "fc(~).second = " << fc("Hello", 7).second << '\n';

    fd();
}

struct MoveOnly
{
    MoveOnly() = default;
    MoveOnly(MoveOnly&&) = default;
};

MoveOnly move_11(MoveOnly arg)
{
    return arg; // OK. 암시적 이동
}

MoveOnly move_11(MoveOnly&& arg)
{
    return arg; // C++20부터 OK. 암시적 이동
}

MoveOnly&& move_23(MoveOnly&& arg)
{
    return arg; // C++23부터 OK. 암시적 이동
}

출력:

fa(1) fb(4) i = 2 fc(~).second = 7 fa(10)

fa(2)는 아무것도 출력하지 않고 바로 돌아오고, fb(5)4를, fb(i)2를 반환해서 i = 2가 출력돼요. return {p, x}로 초기화 목록을 돌려주는 fc도 확인할 수 있고요.

더 알아보기

  • 암시적 변환(implicit casts): 반환 값이 함수 반환 타입으로 변환되는 규칙.
  • 복사 생략(copy elision): C++17 이후 보장된 복사 생략.
  • 코루틴(coroutines): co_return이 쓰이는 마지막 중단 지점.