본문 바로가기

C, C++/Effective C++

[Effective C++] 4장 설계 및 선언

개요

https://product.kyobobook.co.kr/detail/S000001962302

 

Effective C++ | 스콧 마이어스 - 교보문고

Effective C++ | [Effective C++]은 C++ 프로그래밍과 설계 기술을 향상시켜 주는 55가지 명쾌한 테크닉을 모은 책이다.

product.kyobobook.co.kr

 

 

이번 장에서는 C++ 프로그램의 인터페이스 설계를 위한 팁을 담고 있습니다.


Item18: 인터페이스는 올바르게 사용하기에는 쉽게 하고 잘못 사용하기에는 어렵게 만들기.

인터페이스를 설계하면 이후 사용자들이 사용하게됩니다.

좋은 인터페이스라면, 사용자가 이해하기 쉬우며, 실수하기 어려워야합니다.

 

책에서는 Date같은 날짜 초기화를 예시로 들고 있습니다.

Date(int day, int month, int year)

위와 같은 인자를 받는 생성자가 있다면, 기본적으로 순서를 헷갈릴 수도 있고, 숫자를 잘못 넣을 가능성도 많습니다.

 

Date(Day day, Month month, Year year)

따라서 위와 같이 서로 다른 object라면 실수할 가능성이 적어집니다.

 

또한 함수의 인자 혹은 반환 타입에 const를 붙여 잘못사용하는 경우를 줄여줄 수 있습니다.

 

추가적으로 shared_ptr에는 deleter를 지정할 수 있습니다. 

(다만 래핑 클래스를 만들어서 사용하는게 낫지 않나 싶네요)


Item19: 클래스 설계를 타입 설계로 간주하기 

    • 새로운 타입의 객체는 어떻게 생성되고 소멸되는가?
    • 새로운 타입의 객체는 객체 초기화와 객체 대입을 어떻게 수행해야 하는가? (초기화와 대입은 다르다)
    • 새로운 타입의 객체가 값으로 전달될 때 의미하는 것은 무엇인가?(복사 생성자의 동작 생각)
    • 새로운 타입에서 허용되는 값의 범위는 무엇인가? (클래스의 불변성 정의)
    • 새로운 타입이 상속 계층 구조에 포함되는가?
    • 새로운 타입에서 어떤 타입 변환이 허용되어야 하는가? (명시적, 암시적)
    • 새로운 타입에서 어떤 연산자 및 함수가 의미 있는가?
    • 어떤 표준 함수는 제외해야 하는가?
    • 누가 멤버 데이터에 접근할 수 있어야 하는가? (접근 제한자)
    • 새로운 타입이 정말 필요한가?

하나씩 생각해볼만한 주제입니다.


Item20: pass-by-value 보다 pass-by-reference-to-const를 선호하기

꼭 필요한 경우가 아니라면 진짜 복사를 하는 것은 메모리를 많이 잡아먹고 속도도 느릴테니 하지 않겠죠?

 

또한 C에서는 call by reference를 위해 포인터를 넘겼었지만, C++에서는 참조타입으로 넘길 수 있기 때문에 nullptr문제가 생기지 않습니다.

 

추가적으로 일반 타입에서는 그냥 call by value로 넘기는 것이 더 빠를 수 있습니다.

처음에는 어짜피 한번 레지스터에 옮겨서 가져갈텐데 왜 더 빠르지? 라고 생각했습니다, 그런데 일반 값의 경우 넘겼을 때 즉시 접근이 가능하지만, 참조의 경우 주소에 접근해서 값을 가져오기 때문에 캐시히트가 되지 않는 경우 등의 속도 저하 여지가 있습니다.

 

또한 다형성 객체의 경우 잘려서 복사될 여지도 있습니다.

 

결론적으로는 pass by reference를 사용하자는 것이고, 넘겨진 참조(주소)를 접근해 변경할 일이 일반적으로는 없기 때문에 

const까지 하는 것이 일반적입니다.


Item21: 객체를 반환해야 할 때 참조를 반환하려고 하지 마라.

지역 스택 객체에 대한 포인터, 참조 반환은 안됩니다.

스택이 끝나면 스택이 정리되면서 사용하던 메모리가 안전하지 않게 되는데 반환할 사람은 없을 것입니다.

 

추가로 new 등으로 힙에 동적할당한 메모리를 반환한다고 하더라도, 동적할당을 함수 내부에서 했다면, 메모리 할당은 내부에서 하고, 해제는 외부에서 해야하기 때문에 적합하지 않습니다.


Item22: 데이터 멤버를 private으로 선언하기

멤버를 최대한 private으로 선언해야합니다.

만약에 public으로 되어있다면, 직접 접근을 해 캡슐화가 깨질 수 있습니다.

 

멤버의 범위를 제한해야하거나, 접근 전에 특정한 자원을 획득해야하는 등 사용자가 마음대로 값을 변경하면 안되는 등의 문제를 막기 위해서 사용합니다.

 

protected같은 상속관계에서도 문제가 생길 수 있으므로 되도록이면 멤버를 private으로 선언하는 것을 선호합시다.

 


Item23: 멤버 함수보다 비멤버 비프렌드(non-member non-friend) 함수를 선호하기.

일반적으로 멤버 함수는 클래스 내의 private 멤버에 접근할 수 있기 때문에 비멤버 비프렌드멤버에 접근하는 것을 선호하는 것이 좋습니다.

 

필요한 기능을 분류하고 싶다면 namespace를 통해 구분하는 것이 확장성에도 더 좋습니다.


Item24: 모든 매개변수에 대한 형 변환이 적용되어야 할 경우 비멤버 함수를 선언하기.

struct Rational {
    int n, d;
    Rational(int n = 0, int d = 1) : n(n), d(d) {} 

    Rational operator*(const Rational& rhs) const {
        return Rational(n * rhs.n, d * rhs.d);
    }
};

int main() {

    Rational r(3, 5);

    auto a = r * 2;
    auto b = 2 * r;

    return 0;
}

현재 상황에서 a는 가능하지만, b는 불가합니다.

 

a의 경우 2라는 rhs가 int인데 생성자로 인해 int는 Rational로 암시적 변환이 가능합니다.

따라서 r * 2는 Rational의 operator*이 호출됩니다.

 

반대로 b의 경우에는 int의 operator*를 호출하려고하기 때문에 int의 operator가 Rational type의 rhs를 받을 수 없기 때문에 컴파일이 불가합니다.

 

이를 해결하기 위해서는 int타입은 lhs를 Rational로 암시적으로 변환하도록 유도해야합니다.

 

Rational operator*(const Rational& a, const Rational& b) {
    return Rational(a.n * b.n, a.d * b.d);
}

 

위와 같은 lhs, rhs모두 Rational을 받는 operator*를 비멤버함수로서 선언한다면, lhs또한 암시적 변환의 대상이 될 수 있기 때문에 2*r같은 연산도 정상적으로 가능합니다.

 

결론적으로 어떤 함수에 들어가는 모든 파라미터에대한 타입 변환이 필요하다면 비멤버 함수로서 선언해야한다는 것입니다.


Item25: 예외를 던지지 않는 swap 지원을 고려하기.

swap함수의 경우 아래와 같은 방법으로 교환합니다. (일반적인 경우)

_CONSTEXPR20 void swap(_Ty& _Left, _Ty& _Right)
    noexcept(is_nothrow_move_constructible_v<_Ty> && is_nothrow_move_assignable_v<_Ty>) {
    _Ty _Tmp = _STD move(_Left);
    _Left    = _STD move(_Right);
    _Right   = _STD move(_Tmp);
}

 

 move와 참조타입을 사용해서 그렇지 일반적으로 구현하는 swap의 형태와 동일합니다.

객체의 상태를 temp를 사용해 서로 교환하는 것이죠.

 

가능하다면 move ctor, assign을 noexcept로 구현하는 것이 더 편할 것으로 보이지만 책에서는 swap의 특수화에 대해 언급합니다.

 

기본적인 swap의 경우 std::swap이며, std에는 일반적으로 새 함수를 추가할 수 없습니다.

따라서 이를 특수화하기 위해서는 직접만든 swap을 사용하도록 유도해야 합니다. 이를 위해, 

직접 namespace를 만들고 그 안에 swap을 정의합니다.

 

컴파일러는 ADL(argument-dependent lookup)을 통해 함수 이름을 찾기 때문에 namespace 안에있는 타입에 대한 swap을 적절히 찾을 수 있습니다.

 

다만 사용하는 곳에서는 using std::swap을 하는 것이 안전합니다.

만약 일반형 (int 등)을 사용하는 경우에는 일반형이 특정 namespace에 포함되지 않기 때문에 이를 위해 std의 swap이 호출되어야하기 때문입니다.

 

추가적으로 swap은 noexcept로 만들어야 더 빠른 경로로 최적화해줄 수 있습니다. 더 자세한 사항은 이후 항목 29?에서 다룰 것입니다.

 

정리하자면 아래와 같습니다.

1. exception을 던지지 않는 public swap을 만든다.

2. ADL호출을 위해 비멤버 swap을 만들고 비멤버 swap에서 멤버 swap을 호출하는 구조를 만든다(캡슐화)

3. 사용자 시점에서 swap 호출 전에 using std::swap을 통해 swap호출 시 특정 namespace에 속하지 않으면 std::swap이 호출되도록 한다.