Abseil Tip 140 상수(Constant) 처리 안전한 관용구

주간 팁 #140: 상수(Constant) 처리: 안전한 관용구

원래 TotW #140으로 2017년 12월 8일 게시됨
작성자: Matt Armstrong
2020년 5월 6일 업데이트됨

빠른 링크: abseil.io/tips/140


C++에서 상수를 표현하는 가장 좋은 방법은 무엇일까요? 영어에서 “상수”라는 단어의 의미는 명확하지만, 이를 코드에서 올바르게 표현하지 못하는 경우가 많습니다. 여기서는 몇 가지 주요 개념을 정의한 후 안전한 기술 목록을 제공합니다. 더 나아가, 무엇이 잘못될 수 있는지와 C++17 언어 기능이 상수를 표현하는 방법을 어떻게 개선했는지에 대해 설명합니다.


상수란?

C++에서 공식적으로 “상수”를 정의하지는 않습니다. 따라서 다음과 같은 비공식 정의를 제안합니다:

  1. 값(Value): 값은 절대 변하지 않습니다. 예를 들어, 5는 항상 5입니다. 상수를 표현하려면 값을 가져야 하며, 단 하나의 값이어야 합니다.
  2. 객체(Object): 객체는 특정 시점에 값을 가지며, C++에서는 변경 가능한(mutable) 객체가 강조됩니다. 그러나 상수의 경우 변경이 허용되지 않습니다.
  3. 이름(Name): 이름이 있는 상수(named constants)는 단순한 리터럴 상수보다 유용합니다. 변수나 함수는 항상 동일한 값을 가지는 상수 객체로 평가될 수 있습니다.

위의 요소를 모두 고려하여, 우리는 항상 동일한 값으로 평가되는 변수나 함수를 상수라고 부를 수 있습니다.


상수와 관련된 주요 개념

  1. 안전한 초기화(Safe Initialization): 상수는 종종 정적 저장소에 값으로 표현되며, 안전하게 초기화되어야 합니다. 자세한 내용은 C++ 스타일 가이드를 참조하세요.
  2. 연결(Linkage): 연결은 프로그램 내에서 이름이 있는 객체가 몇 개의 인스턴스(또는 “복사본”)로 존재하는지와 관련이 있습니다. 하나의 이름을 가진 상수는 프로그램 내에서 단일 객체를 참조하는 것이 가장 좋습니다. 이를 위해 전역 또는 네임스페이스 범위 변수의 경우 외부 연결(external linkage)이 필요합니다. (연결에 대한 자세한 내용은 여기를 참조하세요.)
  3. 컴파일 시간 평가(Compile-time evaluation): 상수의 값이 컴파일 시간에 알려지면 컴파일러가 코드를 최적화하는 데 더 유리할 수 있습니다. 이는 상수 값을 헤더 파일에 정의하는 복잡성을 감수할 만한 이점이 될 수 있습니다.

상수를 정의하는 방법

상수를 추가한다고 할 때, 우리는 실제로 API를 선언하고 이를 위의 기준을 충족하도록 구현하는 것입니다. C++ 언어는 이를 수행하는 방법을 명시하지 않으며, 어떤 방법은 다른 방법보다 더 나을 수 있습니다. 종종 가장 간단한 접근법은 const 또는 constexpr 변수를 선언하는 것입니다. 이때 헤더 파일에서 사용한다면 inline으로 표시해야 합니다. 다른 접근법으로는 값을 반환하는 함수를 사용하는 것이 있으며, 이는 더 유연합니다. 두 가지 접근법의 예를 살펴보겠습니다.


헤더 파일에서의 상수 정의

다음 섹션에 설명된 방법은 모두 안전하며 추천할 만합니다.

1. inline constexpr 변수

C++17부터 변수에 inline을 사용할 수 있으며, 이를 통해 변수의 복사본이 하나만 생성되도록 보장할 수 있습니다. constexpr와 함께 사용하면 안전한 초기화와 소멸을 보장합니다.

// foo.h
inline constexpr int kMyNumber = 42;
inline constexpr absl::string_view kMyString = "Hello";

2. extern const 변수

// foo.h에서 선언
extern const int kMyNumber;
extern const char kMyString[];
extern const absl::string_view kMyStringView;

이 접근법은 각 객체의 인스턴스를 하나로 제한합니다. extern 키워드는 외부 연결을 보장하고, const는 값의 변형을 방지합니다. 이 방법은 상수 값을 컴파일러가 “볼” 수 없다는 단점이 있지만, 일반적인 사용 사례에서는 문제가 되지 않습니다. 또한 .cc 파일에서 해당 변수를 정의해야 합니다.

// foo.cc에서 정의
constexpr int kMyNumber = 42;
constexpr char kMyString[] = "Hello";
constexpr absl::string_view kMyStringView = "Hello";

3. constexpr 함수

constexpr 함수는 항상 동일한 값을 반환하므로 상수처럼 동작합니다. 이러한 함수는 암시적으로 inline으로 처리되므로 연결 문제는 없습니다.

// foo.h
constexpr int MyNumber() { return 42; }

4. 일반 함수

constexpr 함수가 적합하지 않거나 가능하지 않은 경우 일반 함수가 대안이 될 수 있습니다. 예:

inline absl::string_view MyString() {
  static constexpr char kHello[] = "Hello";
  return kHello;
}

5. 클래스의 static 멤버

클래스의 static 멤버는 이미 클래스와 함께 작업 중인 경우 좋은 선택입니다.

// foo.h에서 선언
class Foo {
 public:
  static constexpr int kMyNumber = 42;
  static constexpr absl::string_view kMyHello = "Hello";
};

잘못된 관행

잘못된 코드 예: 매크로 사용

#define WHATEVER_VALUE 42

매크로는 잘못된 경우가 많습니다. (스타일 가이드 참조).


잘못된 초기화 예: 정적 변수의 동적 초기화

const int kArbitrary = ArbitraryFunction();  // 동적 초기화 (비추천)

동적 초기화는 종종 문제를 일으킵니다. 자세한 내용은 스타일 가이드를 참조하세요.


요약

  • constexpr는 안전한 초기화와 소멸을 보장합니다.
  • inlineconstexpr를 조합하여 헤더 파일에서 상수를 선언할 때 유용합니다.
  • 전역 상수는 .cc 파일에서 정의하고 헤더 파일에서는 extern const로 선언하세요.

추가 읽을거리: