클래스는 C++에서 코드의 근본적인 유닛이다. 자연스럽게, 광범위하게 클래스를 사용한다. 이 섹션에선 클래스 작성 시 지켜야할 해야할 점과 하지 말아야할 점을 나열한다.
Doing Work in Constructors
생성자에서 가상 함수 호출을 피하고, 에러를 보낼 수 없다면 실패할 수 있는 초기화를 피하자.
Definition:
생성자의 몸체에서 임의의 초기화 수행이 가능하다.
Pros:
- 클래스가 초기화되었는지 아닌지에 관해 걱정할 필요가 없다.
- 완전히 생성자 호출에 의해 초기화된 객체는 const 일 수 있으며 표준 container 또는 algorithms으로 사용하기 쉽다.
Cons:
- 만약 가상 함수를 호출한다면, 이러한 호출은 하위클래스 실행으로 전달되지 않을 것이다. 향후 클래스가 현재 하위 클래스가 아닐지라도 조용히 이러한 문제를 일으켜 혼란을 야기한다.
- 생성자가 에러를 전달하거나 프로그램이 크래쉬 나거나 (항상 적절하지 못하게) 또는 예외를 사용하거나(여기서는 금지된) 할 수 있는 쉬운 방식이 없다.
- 만약 실패한다면, 초기화 코드가 실패된 객체를 가지며 이는 호출을 잊기 쉬운 bool isValid() 같은 상태 검사 메커니즘을 필요로하는 unusal 상태가 될 것이다.
- 생성자의 주소를 가질 수 없고, 생성자에서 예를 들어 다른 쓰레드에서 끝난 어떤 작업이든 전달받기 어렵다.
Decision:
생성자는 절대 가상 함수를 호출해선 안된다. 만약 코드가 적절하다면, 프로그램이 종료되는 것은 적절한 에러 핸들링 반응일지 모른다. 반면, TotW#42에 설명된 팩토리 함수나 Init 메서드를 고려하자. 어떤 public 메서드가 호출될지도 모르는 것에 영향을 끼치는 다른 상태가 없는 객체에서 Init 메소드 호출을 피하자.(이러한 형태의 절반만 생성된 객체는 적절히 동작하기 어렵다)
Implicit Conversions
암시적 변환을 정의하지 말자. 변환 연산자와 single-argument 생성자에는 explicit 키워드를 사용하자.
Definition:
암시적 변환은 한가지 타입의 객체가 ( source type으로 불리는) 다른 타입(destination type으로 불리는)으로 기대되는 곳에 사용되도록 한다. 예를 들어 int 인자를 double 파라미터를 취하는 함수에 전달하는 경우
게다가 언어로 정의된 암시적 변환 외에도, source 또는 destination 타입의 클래스 정의에 적절한 멤버를 추가하여 자신만의 변환을 추가할 수 있다. source 타입에서 암시적 변환은 destination 타입 이후 명명된 타입 변환 연산자 (e.g., operator bool())에 의해 정의된다. destination 타입에서 암시적 변환은 인자에서만 (또는 디폴트 값을 가지지 않는 인자) source 타입을 가질 수 있는 생성자에 의해 정의된다.
explicit 키워드는 생성자 또는 변환 연산자에 적용될 수 있으며, destination 타입이 사용 지점에서 명시적일 때(e.g., with a cast)만 사용될 수 있음을 보장한다. 이것은 암시적 변환 뿐 아니라 리스트 초기화 syntax에도 적용할 수 있다.
class Foo {
explicit Foo(int x, double y);
...
};
void Func(Foo f);
Func({42, 3.14}); // Error
이런 코드는 기술적으로는 암시적 변환이 아니지만 explicit에 관한 한 언어는 이를 암시적 변환으로 취급한다.
Procs:
- 암시적 변환은 명백할 때 타입에 명시적으로 이름을 붙일 필요를 제거하여 타입을 더 사용성 있게 하고 표현력있게 할 수 있다.
- 암시적 변환은 오버로딩의 더 단순 대안이 될 수 있다. 예를 들어 string_view parameter를 가진 단순한 함수는 std::string과 const char*를 위한 별도의 오버로드를 대체한다.
- 리스트 초기화 syntax는 객체를 초기화하는간결하고 표현적이다.
Cons:
- 암시적 변환은 타입-불일치 버그를 숨길 수 있는데, 목적 타입이 유저의 기대와 불일치하거나 유저가 어떤 변환이 일어날 것을 인지하지 못하는 경우다.
- 암시적 변환은 코드가 읽기 어렵게 할 수 있는데, 특히 코드가 실제로 호출되는 것이 무엇인지 명박하게 하지 못함으로써 오버로딩 시 읽기 어렵게 한다.
- 단일 인자를 취하는 생성자는 그렇게 설계되지 않았더라도 우연히 암시적 타입 변환으로 사용되기 쉽다.
- 암시적 변환은 호출-위치 모호성을 유발하며, 특히 양방향 암시적 변환이 존재할 때이다. 이는 암시적 변환을 제공하는 두 타입을 가지거나 암시적 생성자와 암시적 타입 변환 연산자를 가진 단일 타입을 가짐으로 써 각각 유발될 수 있다.
- 만약 목적 타입이 암시적이라면, 특히 리스트가 단일 요소를 가질 경우 리스트 초기화는 같은 문제를 야기할 수 있다.
Decision:
타입 변환 연산자와 단일 인자로 호출될 수 있는 생성자는 클래스 정의에서 explicit으로 되어야만 한다. 예외로, 복사와 이동 생성자는 explicit이 되지 않아야만 하는데, 타입 변환을 수행하지 않기 때문이다.
암시적 변환은 교환될 수 있도록 설계된 타입을 위해선 가끔 적절하며 필수적일 수 있다. 예를 들어 두 타입의 객체는 같은 근본적인 값의 표현만 다르다. 이 경우 이 규칙을 어겨도 되는지 프로젝트 리드에게 요청하자.
단일 인자를 가진 채 호출될 수 없는 생성자는 explicit을 생략해도 될지 모른다. 단일 std::initialzier_list 매개변수를 취하는 생성자는 또한 explicit을 생략해야만 하는데, copy-initialization (e.g., MyType m = {1, 2};)를 지원하기 위해서이다.
Copyable and Movable Types
클래스의 public API는 class가 복사 가능한지, 이동만 가능한지 또는 둘다 불가능한지 명확하게 한다. 만약 이러한 연산이 해당 타입에 명확하고 의미가 있다면 복사와 이동을 지원하자.
Definition:
이동 가능한 타입은 임시 객체에서 할당되고 초기화 될 수 있다.
복사 가능한 타입은 같은 타입의 다른 객체에서 할당되고 초기화될 수 있으며 (그러므로 정의에 의해 이동도 가능), 소스의 값이 변하지 않도록 규청한다. std::unique_ptr<int>는 복사는 불가능하지만 이동은 가능한 예시이다.(std::unique_ptr<int> 소스의 값은 목적에 할당 동안 수정되어야만 한다). int와 std::string은 또한 복사 가능한 이동 가능한 타입의 예이다. (int의 경우, move와 copy 연산이 동일하다; std::string의 경우, 복사보다 이동 연산이 더 저렴하다.)
user-defined 타입의 경우, 복사하는 행위는 복사 생성자와 복사 할당 연산자에 의해 정의된다. 이동하는 행위는 이동 생성자와 이동 할당 연산자에 의해 정의되며 존재하지 않다면 복사 연산자와 복사 할당 연산자에 의해 정의된다.
복사/이동 생성자는 암시적으로 몇가지 경우에서 호출될 수 있다, e.g., 값으로 객체를 전달할 때.
Pros:
반환될 수 있는 이동가능하고 복사 가능한 타입의 객체 는 APIs를 더 단순하고 안전하고 일반적으로 만드는 값에 의해 전달될 수 있다. 하지만, 포인터나 참조로 전달한 객체일 때, 소유권, 생명, mutability 그리고 비슷한 이슈에 혼란의 위험이 없으며, 계약서에 서명할 필요도 없다. 또한 클라이언트와 implementation 간 non-local 상호작용을 막는데, 이는 컴파일러에 의해 최적화되고 유지보수하고 이해하기 쉽게 한다. 게다가, 이러한 객체는 대부분의 containers 같은 pass-by-value를 필요로하는 일반적인 API와 함께 사용될 수 있으며, 타입 변환 같은 곳에서 추가적인 유연성을 제공한다.
복사/이동 생성자와 할당 연산자는 주로 Clone(), CopyFrom() 또는 Swap() 같은 대안보다 더 올바르게 정의하기 쉬운데, 왜냐하면 컴파일러가 암시적으로 또는 =default와 함께 생성해주기 때문이다. 이들은 간결하며 모든 데이터 멤버가 복사되는 것을 보장한다. 복사와 이동 연산자는 일반적으로 더 효율적인데, 힙 할당 또는 별도의 초기화와 할당 단계를 필요로 하지 않으며 copy elision 같은 최적화 자격을 가지기 때문이다.
이동 연산은 rvalue 객체에서 외부로 암시적이고 효율적인 자원 전달을 가능하게 한다. 이를 통해 더 단순한 코딩 스타일을 구현할 수 있다.
Cons:
복사될 필요가 없는 몇가지 타입이 있고, 이러한 타입을 위한 복사 연산을 제공하는 것은 혼란스럽거나 말도 안되거나 완전히 잘못될 수 있다. 싱글톤 객체를 등록하는 타입(Register), 특정 scope에 묶여 있는 객체(Cleanup), 또는 객체 식별에 긴밀히 연결된 객체 (Mutex)는 의미적으로 복사될 수 없다. 다형적으로 사용될 수 있는 기본 클래스 타입의 복사 연산은 위험한데, 왜냐하면 객체 slicing으로 이어질 수 있기 때문이다. 기본값 또는 주의깊게-구현되지 않은 복사 연산은 잘못될 수 있으며 이러한 버그는 발견하기 어렵고 혼란스러울 수 있다.
복사 생성자는 암시적으로 호출되며, 이는 놓치기 쉬운 호출이다. 이는 pass-by-reference가 일반적이거나 필수인 언어에 익숙한 프로그래머에게 혼란을 야기할지도 모른다. 성능 문제를 유발할 수 있는 과도한 복사를 유발할지 모른다.
Decision:
모든 클래스의 public 인터페이스는 클래스가 지원하는 복사와 이동 연산이 어떤 것인지 명확히 해야한다. 이는 주로 선언의 public 섹션에서 적절한 연산을 명시적으로 선언하거나 삭제하는 형태를 취해야만한다.
특히, 복사 가능한 클래스는 명시적으로 복사 연산을 선언해야하고, 이동만 가능한 클래스는 명시적으로 이동 연산을 선언해야만 하고, 이동과 복사 불가능한 클래스는 복사 생성자를 삭제해야만 한다. 이동 가능한 클래스는 또한 효율적인 이동을 지원하기 위해 이동 연산를 선언할지도 모른다. 명시적으로 모든 4 종류의 복사/이동 연산을 삭제하거나 선언하는 것은 가능하지만 필수는 아니다. 만약 복사 또는 이동 할당 연산자를 제공한다면, 일치하는 생성자를 또한 제공해야만 한다.
class Copyable {
public:
Copyable(const Copyable& other) = default;
Copyable& operator=(const Copyable& other) = default;
// The implicit move operations are suppressed by the declarations above.
// You may explicitly declare move operations to support efficient moves.
};
class MoveOnly {
public:
MoveOnly(MoveOnly&& other) = default;
MoveOnly& operator=(MoveOnly&& other) = default;
// The copy operations are implicitly deleted, but you can
// spell that out explicitly if you want:
MoveOnly(const MoveOnly&) = delete;
MoveOnly& operator=(const MoveOnly&) = delete;
};
class NotCopyableOrMovable {
public:
// Not copyable or movable
NotCopyableOrMovable(const NotCopyableOrMovable&) = delete;
NotCopyableOrMovable& operator=(const NotCopyableOrMovable&)
= delete;
// The move operations are implicitly disabled, but you can
// spell that out explicitly if you want:
NotCopyableOrMovable(NotCopyableOrMovable&&) = delete;
NotCopyableOrMovable& operator=(NotCopyableOrMovable&&)
= delete;
};
이러한 선언/삭제는 명백할 경우에만 생략할 수 있다.
- 만약 클래스가 struct나 interface-only 기본 클래스 처럼 private 섹션을 가지지 않는다면, 복사가능성/이동가능성은 어떤 public data member의 복사가능성/이동가능성에 의해 결정될 수 있다.
- 만약 기본 클래스가 명확하게 복사 또는 이동 가능하지 않을 경우, 파생 클래스는 일반적으로 그럴 것이다. 이러한 연산을 암시적으로 남겨둔 인터페이스-only 기본 클래스는 구현 서브클래스를 명확하게 하기에는 충분하지 못하다.
- 만약 명시적으로 복사를 위한 생성자 또는 할당 연산자를 각각 명시적으로 선언하거나 삭제할 경우, 다른 복사 연산은 명백하지 않고 선언되거나 삭제되어야만 한다. 이동 연산의 경우도 마찬가지다.
만약 복사.이동의 의미가 일반 유저에게 명확하지 않거나 아니면 예상치 못한 비용을 발생할 경우 타입은 이동/복사 불가능해야만 한다. 복사 가능한 타입의 이동 연산은 엄밀히 말하면 성능 최적화이며 버그와 복잡성의 잠재적 원인이 되므로 해당 복사 작업보다 훨씬 효율적이지 않는 한 정의를 피하자. 만약 해당 타입이 복사 연산을 제공한다면, 해당 연산의 기본 구현이 올바른 클래스를 설계하는 것이 좋다. 다른 코드와 마찬가지로 기본 설정된 작업의 정확성을 검토하는 것이 좋다.
slicing 위험을 제거하기 위해, 생성자를 protect로 만들거나 소멸자를 protect로 만들나 한 개 이상의 순수 가상 멤버 함수를 제공해 기본 클래스는 추상적으로 만드는 것을 선호하자. 구체 클래스로부터 파생되는 것을 피하자.
Structs vs. Classes
struct를 데이터를 전달하는 패시브 객체에만 사용하자; 다른 모든 것은 class이다.
struct와 class 키워드는 C++에서 거의 동일하게 행동한다. 우리는 각 키워드에 시맨틱 의미를 추가하며, 본인이 정의한 데이터-타입을 위한 적절한 키워드를 사용해야만 한다.
struct는 데이터를 전달하는 패시브 객체에만 사용되어야만 하며 관련된 상수를 가질수도 있다. 모든 필드는 public이어야만 한다. struct는 다른 필드간의 관계를 암시하는 불변성을 가지면 안되는데, 이러한 필드에 직접적인 유저 접근은 이러한 불변성을 깨뜨릴 수 있을지도 모르기 때문이다. 생성자, 소멸자 그리고 helper 메서드는 존재할 수 있다; 하지만, 이러한 메서드는 어떤 불변성을 필요로 하거나 강제해서는 안된다.
만약 더 많은 함수성 또는 불변성이 필요하거나 아니면 struct가 넓은 시야 그리고 진화할 것으로 예상된다면, class가 더 적절하다. 만약 의심스럽다면, class로 만들어라.
STL과의 일관성을 위해, traits, template metafunctions 그리고 몇몇 functors 같은 stateless 타입의 경우 class 대신 struct를 사용할 수 있다.
structs와 classes에서 멤버 변수는 다른 네이밍 룰을 가진다.
Struct vs. Pairs and Tuples
엘리멘트가 의미있는 이름을 가질 수 있다면 pair 또는 tuple 대신 struct 사용을 선호하자.
pairs와 tuples 사용은 코드를 작성할 때 작업을 절약해 커스텀 타입을 정의할 필요성을 피할 수 있지만, 의미있는 필드 이름은 .first, .second 또는 std::get<X> 보다 코드를 읽을 때 항상 더 명확하게 한다. (타입이 유니크할 때) 인덱스보다 타입에 의한 tuple element에 접근이 std::get<Type>으로 C++14에 소개가 가끔 이것을 가끔 부분적으로 완화할 순 있지만, 필드 이름은 주로 타입보다 실질적으로 명확하고 더 유익하다.
Pairs와 tuple은 pair 또는 tuple의 요소가 구체적인 의미가 없을 경우 일반 코드에서 적절할 지도 모른다. 이것들의 사용은 기존 코드 또는 APIs와 상호운용하기 위해 필요할 수도 있다.
Inheritance
Composition은 주로 Inheritance보다 더 적절하다. inheritance 사용 시 public을 사용하자.
Definition:
sub-class가 base class으로부터 inherit될 경우, base class에서 정의한 모든 데이터와 연산의 정의를 포함한다. "Interface inheritance"는 순수 추상 base class (상태나 정의된 메서드가 없는 클래스)으로부터 상속이다; 모든 다른 상속은 "implementation inheritance"이다.
여기서 상태가 없다는 의미는 멤버 변수가 없다는 의미이다.
Pros:
Implementation inheritance는 기존 타입을 전문화하기에 base class code를 재사용하여 코드 사이즈를 줄여준다. 상속은 컴파일-타임 선언이기 때문에, 컴파일러는 연산을 이해할 수 있고 에러를 감지할 수 있다. 인터페이스 상속은 프로그래머적으로 클래스가 특정 API를 노출하도록 강제한다. 다시, 이 경우 클래스가 필수 API 메서드를 정의하지 않을 경우, 컴파일러는 에러를 감지할 수 있다.
Cons:
Implementation inheritance의 경우, sub-class를 구현하는 (implementing) 코드는 base와 sub-class 간 분산 되기 때문에, implementation을 이해하기 더 어렵게 할 수 있다. sub-class는 virtual 이 아닌 함수를 override할 수 없기에 sub-class는 implementation을 변경할 수 없다.
다중 상속은 특히 문제가 될 수 있는데, 더 높은 성능 오버 헤드를 부과하기 때문이며(사실, 단일 상속에서 다중 상속으로 사용 시 성능 감소가 일반 함수를 가상함수로 호출할 때의 성능 감소보다 일반적으로 더 클 수 있다), 모호하고, 혼란스럽고 노골적인 버그를 일으킬 수 있는 "diamond" 상속 패턴으로 이끄는 위험이 있기 때문이다.
Decision:
모든 상속은 public이어야만 한다. 만약 private 상속을 원하면, base class의 instance를 멤버로 대신 포함해야만 한다. base class로서 이들을 사용하는 것을 지원하지 않을 의도면 final를 사용할 수도 있다.
implementation inheritance를 과사용 하지 말자. Composition은 주로 더 적절하다. "is-a"인 경우로 상속의 사용을 제한하자: 만약 합리적으로 Bar가 Foo의 한 종류로 말할 수 있으면, Bar 는 Foo를 subclass한다.
subclass에서 접근될 필요가 있는 protected 멤버 함수 사용을 제한하자. data members는 private이어야만 한다.
명시적으로 overrides 또는 final specifier 중 하나만 사용해서 virtual 소멸자 또는 virtual function의 override를 사용하자. override를 선언할 때 virtual을 사용하지 말자. 근거: base 클래스 가상 함수의 override가 아닌 override 또는 final로 마크된 함수 또는 소멸자는 컴파일되지 않을 것이며 이는 일반적인 에러를 잡는데 도움이 된다. specifier는 문서로서 제공된다; 만약 specifier가 없다면, reader는 만약 함수 또는 소멸자가 virtual인지 아닌지 결정해야하는 의문을 가지고 클래스의 모든 조상을 체크해야만 한다.
다중 상속은 허용되지만 다중 implementation inheritance는 강력하게 비권장된다.
final 키워드
final 키워드를 명시적으로 작성하게 되면, 컴파일러에게 가상함수로 호출되는 마지막이라는 메시지를 주기 때문에 컴파일 최적화에 도움이 된다.
Operator Overloading
operator를 신중히 overload하자. user-defined literals를 사용하지 말자.
Definition:
C++는 user code가 파라미터 중 하나가 user-defined type 일 경우 operator 키워드를 사용해 built-in operators의 overload된 버전을 선언하는 것이 가능하다. operator 키워드는 또한 user code가 operator""를 사용해 새로운 종류의 literals를 정의하는 것과 operator bool() 같은 타입-변환 함수를 정의하는 것이 가능하다.
Pros:
Operator overloading은 core를 더 간결하게 만들 수 있고 built-in 타입처럼 같은 행동하는 사용자 정의된 타입을 가능하게 하여 직관적이게 한다. overload된 operators는 특정 연산의 관용적인 이름이며(e.g., ==, <, =, and <<), 이러한 관습이 user-defined 타입을 더 읽기 쉽게 만들고 이러한 이름을 기대하는 라이브러리와 상호운용되도록 한다.
User-defined literals은 user-defined 타입의 객체를 생성하는 매우 간결한 notation이다.
Cons:
- 올바르고, 일관적이고 놀라지 않는 operator overload set를 제공하는 것은 약간의 주의를 요구하며, 이렇게 하지 않으면 버그와 혼란을 야기한다.
- operators의 과도한 사용은 특히, overload된 operator의 semantics가 관습을 따르지 않는다면, 난독화된 코드로 이어질 수 있다.
- 연산자 오버로딩 문제처럼 함수 오버로딩의 위험성은 똑같이 적용되며 경우에 따라 더 심각할 수 있다.
- 연산자 오버로딩은 비싼 연산을 싼 연산으로, built-in operation으로 생각하게 만들어 직관을 속일 수 있다.
- 오버로드 된 연산자의 호출 위치는 찬는 것은 e.g., grep 보다 C++ syntax의 검색 툴을 필요로 할 수 있다.
- 만약 오버로드 된 연산자의 인자 타입이 잘못되었다면, 컴파일 에러보다 다른 오버로드를 얻을 수 있다. 예를 들어, foo < bar는 한가지 일지 모르지만, &foo < &bar는 가끔 완전히 다르게 동작된다.
- 특정 연산자 오버로드는 본질적으로 위험하다. unary & 오버로딩은 같은 코드를 오버로드 선언이 볼 수 있는지에 따라 다른 의미를 가지도록 유발할 수 있다. &&, ||, and ,(comma)의 오버로드는 built-in 연산자의 평가 순서 시맨틱과 일치하지 않을 수 있다.
- 연산자는 주로 클래스 외부에 정의되며, 같은 연산의 다른 정의를 가지는 다른 파일의 위험이 있다. 만약 같은 binary로 링크된 정의가 있다면, 이는 undefined behavior이며, 미묘한 런타임 버그로 나타날 수 있다.
- User-defined literals (UDLs)은 "Hello World"sv가 std::string_view("Hello World")로 요약된 것처럼, 숙련된 C++ 프로그래머조차 친숙하지 않은 새로운 syntatic 형태의 생성을 가능하게 한다. 기존 notations이 더 명확하다.
- 이들은 namespace-qualified일 수 없기 때문에, UDLs의 사용은 user-directives(우리는 금지한다) 또는 using-declarations(import된 이름이 question에서 header file에 의해 노출된 인터페이스 부분일 경우를 제외하고 header file에서는 금지한다)의 사용을 요구한다. 만약 헤더 파일이 UDL suffixes를 피해야만 한다면,header files와 source files 간 literals의 관습을 다르게 가지는 것을 피하는 것을 선호한다.
Decision:
의미가 명확하고 놀라지않고 buil-in 연산자와 일치하는 일관성이 있을 경우에만 오버로드 된 연산자를 정의하자. 예를 들어, | 연산자는 bitwise- 또는 logical-or로 사용하며, shell-type pipe로는 사용하지 않는다.
본인 소유의 타입에서만 연산자를 정의하자. 더 정확하게는, 이들이 동작하는 타입으로써 같은 헤더, .cc 파일, namespace 에서 연산자를 정의하자. 즉, 연산자는 타입이 어디에 있든 사용 가능하고, 중복 정의의 위험을 최소화한다. 가능하다면, templates으로 연산자 정의하는 것은 피하자. 왜냐하면 어떤 가능한 template 인자의 규칙을 만족해야만 하기 때문이다. 만약 operator를 정의한다면, 의미가 있는 관련된 연산자를 정의하고 일관적으로 정의되도록 보장하자.
비 멤버 함수로 수정할 수 없는 binary 연산자를 정의하는 것을 선호하자. 만약 binary 연산자가 클래스 멤버일 경우, 암시적 변환은 left-hand 가 아닌 right-hand 인자로 적용될 것이다. 이는 a + b는 컴파일되지만, b + a는 컴파일 되지 않아 혼란스럽다.
타입 T의 값은 동일성을 비교할 수 있는 경우, 타입 T의 값이 비 멤버 operator==로 정의하자고 언제 타입 T의 값이 동등한지 문서화하자. 만약 타입 T의 값 t1이 다른 값 t2보다 작을 경우 하나의 명확한 언급이 있다면, operator<=>을 정의할 수도 있으며, operator==과 일관성을 유지해야만 한다. 다른 comparsion and ordering operators를 오버로드 하지 않는 것을 선호하자.
반대로 연산자 오버로딩을 정의하는 것을 회피하지 말자. 예를 들어, ==, =, and << 은 Equals(), CopyFrom(), and PrintTo() 보다 정의하기 좋다. 반대로, 다른 라이브러리가 연산자 overload를 기대한기 때문에 단순히 정의하지는 말자. 예를 들어, 자연스런 순서가 없는데, std::set에 저장하고 싶을 경우, overloading < 보다 custom operator를 사용하자.
&&, ||, ,(comma), 또는 unary &를 오버로드 하지 말자. operator"", i.e., user-defined literals를 오버로드 하지 말자. 이러한 다른 곳에서 (including the standard library) 제공된 literals 같은 어떠한 것도 사용하지 말자.
타입 변환 연산자는 implicit conversions 섹션에서 다룬다. = 연산자는 copy constructurs 섹션에서 다룬다. stream과 같이 사용을 위한 << 오버로딩은 streams 섹션에서 다룬다. function overloading 또한 operator overloading에 적용되는 동일한 규칙이다.
Access Control
클래스의 데이터 멤버가 constants가 아니면 private로 만들자. 이는 필요하다면 (주로 const) accessors의 형태에서 몇몇 쉬운 boilerplate의 비용에서 불변성에 관한 추론을 단순화한다.
기술적인 이유로, Google Test 사용 시 protected 된 .cc 파일에서 정의된 test fixture class의 데이터 멤버를 허용한다. 만약 test fixture class가 .cc 파일 외부에 정의되고 사용된다면, 예를 들어 .h file, private 데이터 멤버로 만들자.
Declaration Order
비슷한 선언을 같이 그룹화하고 public 파트에 쉽게 위치시키자.
클래스 정의는 주로 public: 섹션으로 시작해야만 하고, protected:, private: 순으로 와야한다. 빈 경우 섹션을 생략하자.
각 섹션에서, 비슷한 종류의 선언이 함께 그룹화하는 것을 선호하고 다음 순서를 선호하자:
- 타입들과 타입 aliases (typedef, using, enum, nested structs and classes, and friend 타입)
- (선택적으로, struct의 경우에만) non-static data member
- Static constants
- Factory functions
- Constructors and assignment operators
- Destructor
- All other functions (static and non-static member functions, and friend functions)
- All other data members (static and non-static)
클래스 정의에 큰 메서드 정의를 inline으로 두지 말자. 주로, 사소하거나 performance-critical 그리고 매우 작은 메서드만 inline으로 정의될 수 있다. inline functions에 자세히 보자.
'C++ > Google C++ Style Guide' 카테고리의 다른 글
| Other C++ Features (0) | 2025.05.26 |
|---|---|
| Google-Specific Magic (0) | 2025.05.26 |
| Functions (1) | 2025.05.25 |
| Scoping (0) | 2025.05.15 |
| Header Files (0) | 2025.05.15 |