Skip to content

Con: 상수와 불변성

상수에 대해서는 경쟁 상태가 발생하지 않는다. 프로그램 내의 객체 중 그 값이 바뀔 수 없는 것이 많다면 해석하기가 쉬울 것이다. 인자로 넘어가는 개체의 상태가 바뀌지 않는 것을 보장한다면 그 인터페이스는 가독성이 굉장히 높을 것이다.

상수 규칙 요약:

Con.1: 기본적으로 객체를 변경 불가능하도록 만들어라

Reason

변하지 않는 개체는 이해하기가 더 쉽다. 값을 바꿀 필요가 있을 때에만 개체를 비-const로 만들어라.

의도하지 않았거나 알아차리기 어려운 값 변경을 예방한다.

Example
    for (const int i : c) cout << i << '\n';    // just reading: const

    for (int i : c) cout << i << '\n';          // BAD: just reading
Exception

함수의 인자들의 값이 변경되는 경우는 드물지만, 또한 이들이 const 로 선언되는 경우 역시 드물다. 혼동과 수많은 거짓 양성 (false positives) 테스트 결과를 피하기 위해, 이 규칙은 함수의 인자들에 대해서는 적용하지 않도록 한다.

    void f(const char* const p); // pedantic
    void g(const int i);        // pedantic

함수 내부의 변수는 지역 변수이므로 이를 변경하는 영향도 지역적임을 짚고 가자.

Enforcement
  • 값이 변경되지 않는 비-const 변수들을 지적하라 (수많은 거짓 양성 테스트 결과를 피하기 위해 인자들에 대해서는 예외로 한다)

Con.2: 기본적으로 멤버 함수들은 const로 만들어라

Reason

멤버 함수가 해당 객체의 상태를 변경하지 않는다면 const로 표시하도록 한다. 이렇게 하면 디자인 의도를 더욱 명확하게 드러내고, 가독성을 높이며, 컴파일러가 더 많은 오류를 잡아낼 수 있게 도우며, 때에 따라서는 최적화를 더 잘 할 수 있도록 한다.

Example; bad
    class Point {
        int x, y;
    public:
        int getx() { return x; }    // BAD, should be const as it doesn't modify the object's state
        // ...
    };

    void f(const Point& pt) {
        int x = pt.getx();          // ERROR, doesn't compile because getx was not marked const
    }
Note

비-const 형식의 포인터나 참조자를 전달하는 것 자체가 근본적으로 나쁜 것은 아니지만, 이는 호출되는 함수가 해당 객체를 수정하는 경우에만 수행되어야 한다. 코드를 읽는 독자는 "plain" T*T& 를 전달받는 함수가 해당 참조가 가리키는 객체를 수정할 것이라고 가정해야 한다. 비록 당장 현재에는 객체를 수정을 하지 않더라도, 이후에 재컴파일을 강제하지 않고도 객체를 수정하도록 변경될 수 있다.

Note

기존 코드/라이브러리 중 일부는 함수 내에서 T를 수정하지 않으면서도 T* 로 선언된 함수를 제공하기도 한다. 이는 코드를 "현대화(modernizing)" 하려는 사람들에게 문제가 될 수 있다. 다음 방법들을 사용할 수 있다

  • 정확하게 const를 이용하도록 라이브러리를 업데이트한다; 장기적인 대안으로 좋다
  • "const속성을 타입 변환(cast)을 통해 해지한다"; 가능한 피하는 것이 좋다
  • wrapper 함수를 제공한다

Example:

    void f(int* p);   // old code: f() does not modify `*p`
    void f(const int* p) { f(const_cast<int*>(p)); } // wrapper

이렇게 wrapper를 이용하는 해결책은(라이브러리 안에 들어 있어 직접 수정할 수 없다든가 하는 이유로) f()의 선언을 수정할 수 없는 경우에만 사용해야 하는 임시방편임에 유의하라.

Note

const 멤버 함수도 mutable 하거나 포인터 멤버를 통해 접근되는 객체에 대해서는 값을 수정할 수 있다. 이러한 방식의 흔한 예는 복잡한 계산을 반복적으로 하지 않기 위해 캐시를 유지하는 경우이다. 예를 들어, 아래 Date 는 반복적인 사용을 단순화하기 위해 날짜의 문자열 표현을 캐시(기억) 하고 있다:

    class Date {
    public:
        // ...
        const string& string_ref() const
        {
            if (string_val == "") compute_string_rep();
            return string_val;
        }
        // ...
    private:
        void compute_string_rep() const;    // compute string representation and place it in string_val
        mutable string string_val;
        // ...
    };

이에 대해서 다른 방식으로 이야기하면, const함은 전이적(transitive) 이지 않다는 것이다. const 멤버 함수에게 있어 mutable 한 멤버의 값과 비-const 포인터를 통해 접근하는 객체의 값을 변경할 수 있기 때문이다. 클래스가 사용자들에게 제공하는 의미론(불변 조건)에 의거하여 타당하다고 여겨질 경우에만 이러한 값 변경이 이루어질 수 있도록 보장하는 것은 해당 클래스의 역할이다.

See also: Pimpl

Enforcement
  • const 표시가 되어 있지 않지만, 어떠한 멤버 변수에 대해서도 비-const 작업을 수행하지 않는 멤버 함수를 지적하라

Con.3: 기본적으로 포인터와 참조는 const로 전달하라

Reason

호출되는 함수가 예상치 못하게 값을 변경하는 것을 방지한다. 호출되는 함수가 상태를 변경하지 않는다면 프로그램의 동작을 예상하기가 훨씬 수월하다.

Example
    void f(char* p);        // does f modify *p? (assume it does)
    void g(const char* p);  // g does not modify *p
Note

비-const 형식의 포인터나 참조자를 전달하는 것 자체가 근본적으로 나쁜 것은 아니지만, 이는 호출되는 함수가 해당 객체를 수정하는 경우에만 수행되어야 한다.

Note

const속성을 타입 변환을 통해 없애지 말라.

Enforcement
  • 객체에 대한 포인터나 참조자를 비-const 형식으로 전달받지만 해당 객체를 수정하지는 않는 함수들을 지적하라
  • 객체에 대한 포인터나 참조자를 const 형식으로 전달받지만 (타입 변환을 통해) 해당 객체를 수정하는 함수를 지적하라

Con.4: 개체 생성 이후 변하지 않는 값은 const로 정의하라

Reason

예기치 않은 객체의 값 변경으로 인해 뜻밖의 결과가 발생하는 것을 방지한다.

Example
    void f()
    {
        int x = 7;
        const int y = 9;

        for (;;) {
            // ...
        }
        // ...
    }

xconst 형식으로 선언되지 않았으므로, 해당 변수는 반복문 내 어디선가 값이 변경된다고 가정해야 한다.

Enforcement
  • 변경되지 않는 비-const 변수들을 지적하라.

Con.5: 컴파일 시간에 계산될 수 있는 값은 constexpr을 사용하라

Reason

성능이 향상되고, 컴파일-시점 검사가 원활해지며, 컴파일-시점 연산(evaluation)이 보장되며, 경합 조건 (race condition) 위험도 피할 수 있다.

Example
    double x = f(2);            // possible run-time evaluation
    const double y = f(2);      // possible run-time evaluation
    constexpr double z = f(2);  // error unless f(2) can be evaluated at compile time
Note

See F.4.

Enforcement
  • 상수 표현식을 통해 초기화되는 const 선언을 지적하라.