본문 바로가기
개발/C++ (98,03,11,14,17,20,23)

Modern C++ : const reference and Value Semantics

by snowoods 2025. 8. 12.

Modern C++

const 참조와 값 의미론 (const Reference and Value Semantics)

 

개요

C++에서 함수에 인자를 전달하는 방식은 프로그램의 성능과 안정성에 큰 영향을 미칩니다. 특히 const 참조를 사용하는 것은 C++ 프로그래머가 반드시 이해해야 할 중요한 개념입니다. 이 문서는 값에 의한 전달(pass-by-value)과 참조에 의한 전달(pass-by-reference), 그리고 const 참조를 사용한 전달 방식의 차이점과 장점을 설명합니다.

  • 값 의미론 (Value Semantics): 함수 호출 시 인자의 복사본을 만들어 전달합니다. 원본 데이터는 함수 내에서 변경되지 않으므로 안전하지만, 객체가 클 경우 복사 비용이 발생하여 성능 저하의 원인이 될 수 있습니다.
  • 참조 의미론 (Reference Semantics): 객체의 복사본 대신 참조(별칭)를 전달합니다. 복사가 발생하지 않아 효율적이지만, 함수 내에서 원본 객체를 수정할 수 있습니다.
  • Const 참조 (Const Reference): 참조의 효율성과 값의 안전성을 결합한 방식입니다. 객체를 복사하지 않으면서도 함수 내에서 원본이 수정되는 것을 컴파일러 수준에서 방지합니다.

 

C++ 버전별 주요 키워드 도입 시기

  • C++98: const 키워드와 참조(&)는 C++ 초기 버전부터 존재했습니다. 이들을 조합한 const 참조는 전통적으로 C++에서 효율성과 안전성을 모두 잡기 위한 핵심적인 기능으로 사용되었습니다.
  • C++11: 예제 코드에 사용된 std::arraystd::uint32_t 같은 고정 크기 정수 타입이 표준에 도입되었습니다. std::array는 C 스타일 배열보다 안전하고 편리한 기능을 제공합니다.

 

내용 설명

함수에 인자를 전달하는 세 가지 주요 방식을 예제 코드와 함께 살펴보겠습니다.

  1. const std::array<std::uint32_t, N> &arr (const 참조에 의한 전달)
    • print_array_values 함수는 배열을 화면에 출력하기만 할 뿐, 내용을 수정하지 않습니다.
    • 이 경우, const 참조를 사용하여 불필요한 데이터 복사를 피하고(성능 향상), 동시에 함수가 원본 배열을 실수로 변경하는 것을 막습니다(안전성 확보).
    • & 기호는 이 매개변수가 참조임을 나타내며, const는 참조하는 대상을 변경할 수 없음을 의미합니다.
  2. std::array<std::uint32_t, N> &arr (참조에 의한 전달)
    • double_value 함수는 배열의 각 요소 값을 2배로 변경해야 합니다. 즉, 원본 데이터를 수정해야 합니다.
    • 이럴 때는 const를 제외한 참조(&)를 사용하여 원본 배열에 직접 접근하고 수정할 수 있도록 합니다.
    • 이 방식 역시 데이터 복사가 발생하지 않아 효율적입니다.
  3. std::array<std::uint32_t, N> arr (값에 의한 전달 - 예제에는 없음)
    • 만약 함수 시그니처가 void some_function(std::array<std::uint32_t, N> arr) 였다면, 함수가 호출될 때 my_array의 완전한 복사본이 생성되어 arr에 전달됩니다.
    • 함수 내에서 arr의 내용을 아무리 변경해도 원본인 my_array에는 아무런 영향이 없습니다.
    • int, double 같은 작은 기본 타입에는 유용하지만, std::arraystd::vector 같은 큰 컨테이너에는 복사 비용 때문에 거의 사용되지 않습니다.

 

예제 코드

#include <array>
#include <cstdint>
#include <iostream>

// const 참조 전달: 입력 전용이며, 복사 비용이 없고, 원본 수정이 불가능하다.
template <std::size_t N>
void print_array_values(
    const std::array<std::uint32_t, N> &arr) 
{
    for (std::size_t i = 0; i < arr.size(); i++)
    {
        std::cout << arr[i] << '\n';
    }
}

// 비-const 참조 전달: 입력과 출력이 모두 가능하며, 원본 수정이 목적이다.
template <std::size_t N>
void double_value(
    std::array<std::uint32_t, N> &arr) 
{
    for (std::size_t i = 0; i < arr.size(); i++)
    {
        arr[i] = arr[i] * 2;
    }
}

int main()
{
    auto my_array = std::array<std::uint32_t, 5U>{1, 2, 3, 4, 5};

    std::cout << "--- Initial Values ---\n";
    print_array_values(my_array); // const 참조로 전달 (복사 없음)

    double_value(my_array); // 비-const 참조로 전달 (원본 수정)

    std::cout << "\n--- Values after doubling ---\n";
    print_array_values(my_array); // const 참조로 전달 (복사 없음)

    return 0;
}

 

실행 결과

--- Initial Values ---
1
2
3
4
5

--- Values after doubling ---
2
4
6
8
10

 

활용팁

  • 읽기 전용(Input-only) 파라미터: 객체가 크거나 복사 비용이 비싸다면 항상 const 참조를 사용하세요. intdouble 같은 기본 타입(primitive types)은 크기가 작아 값으로 전달해도 무방합니다.
  • 수정 가능(Input/Output) 파라미터: 함수가 원본 객체를 수정해야 한다면 비-const 참조를 사용하세요. 포인터를 사용할 수도 있지만, 참조가 일반적으로 더 안전하고 사용하기 쉽습니다. (예: nullptr 체크 불필요)
  • 소유권 이전: C++11 이후로는 std::move와 r-value 참조를 통해 객체의 소유권을 효율적으로 이전하는 방법도 있습니다. 이는 복사를 피하면서 객체를 전달하는 또 다른 중요한 메커니즘입니다. (관련 문서: l-value/r-value, std::move)