본문 바로가기
C++/Google C++ Style Guide

Google C++ Style Guide : 클래스(Classes)

by 개발자J의일상 2022. 6. 2.
반응형

Classes

클래스는 C++에서 코드의 기본 단위이다. 자연스럽게 우리는 클래스를 광범위하게 사용한다.

이 섹션은 클래스를 작성할 때 따라야 하는 주요 사항과 하지 말아야 할 사항을 나열한다.


생성자에서의 작업(Doing Work in Constructors)

생성자에서 가상 메서드를 호출하지 않도록 하고, 오류 신호를 보낼 수 없는 경우 초기화가 실패할 수 있는 것을 방지한다.

 

정의:

생성자의 body에서 임의의 초기화를 수행할 수 있다.

 

장점:

  • 클래스가 초기화되었는지 여부에 대해 걱정할 필요가 없다.
  • 생성자 호출에 의해 완전히 초기화된 객체는 const일 수 있으며 표준 컨테이너 또는 알고리즘과 함께 사용하기 더 쉬울 수 있다.

단점:

  • 작업이 가상 함수를 호출하는 경우, 이러한 호출은 하위 클래스 구현으로 전달되지 않는다. 나중에 클래스를 수정하여 클래스가 현재 하위 클래스가 아닌 경우에도 이 문제가 조용히 발생하여 많은 혼란을 일으킬 수 있다.
  • 프로그램 충돌(항상 적절한 것은 아님)이나 예외(금지됨)를 사용하지 않는 한 생성자가 오류 신호를 보내는 쉬운 방법은 없다.
  • 작업이 실패하면 초기화 코드가 실패한 객체가 있으므로 호출하는 것을 잊어버리기 쉬운 bool IsValid() 상태 검사 매커니즘(또는 이와 유사한)이 필요한 비정상적인 상태일 수 있다.
  • 생성자의 주소를 가져올 수 없으므로 생성자에서 수행된 작업은 예를 들어 다른 스레드에 쉽게 넘겨줄 수 없다.

 

결정:

 

생성자는 가상 함수를 절대로 호출해서는 안된다. 여러분의 코드에 적합한 경우, 프로그램을 종료하는 것이 적절한 오류 처리 응답일 수 있다. 그렇지 않으면 TotW #42에 설명된 대로 팩토리 함수 또는 Init() 메서드를 고려해라.

어떤 public 메서드가 호출될 수 있는지에 영향을 주는 다른 상태(state)가 없는 객체에서는 Init() 메서드를 사용하지 마라(이 형식의 semi-constructed 객체는 특히 올바르게 작업하기 어렵다)


암시적 변환(Implicit Conversion)

암시적 변환을 정의하지 마라.

변환 연산자 및 단일 인수 생성자에 대해 명시적 키워드(explicit keyword)를 사용한다.

 

정의:

암시적 변환은 한 유형의 객체(소스 유형이라고 부름)가 다른 유형(대상(destination) 유형이라고 부름)으로 사용되기가 기대되는 경우, 예를 들어 double 매개변수를 받는 함수에 인자로 int 타입을 전달하는 경우에 허용한다.

 

언어에 의해 정의된 암시적 변환 외에도 사용자는 소스 또는 대상 유형의 클래스 정의에 적절한 멤버를 추가하여 자신의 변환을 정의할 수 있다.

소스 유형의 암시적 변환은 대상 유형의 이름을 따서 명명된 유형 변환 연산자(예: 연산자 bool())에 의해 정의됩니다.

대상 유형의 암시적 변환은 소스 유형을 유일한 인수(또는 기본값이 없는 유일한 인수)로 사용할 수 있는 생성자에 의해 정의된다.

 

explicit 키워드는 생성자 또는 변환 연산자에 적용하여 대상 유형이 사용 지점(예: 캐스트와 함께)에서 명시적일 때만 사용할 수 있도록 할 수 있다. 이것은 암시적 변환뿐만 아니라 list 초기화 구문에도 적용된다.

 

※ 여기서 explicit 키워드는 자신이 원하지 않는 형변환이 일어나지 않도록 제한하는 키워드이다.

class Foo {
  explicit Foo(int x, double y);
  ...
};

void Func(Foo f);
Func({42, 3.14});  // Error
Func(Foo(42, 3.14)); // OK

explicit 키워드가 없었다면 위의 Error는 암시적 형변환이 일어나서 정상적으로 Foo의 생성자가 호출되었을 것이다.

 

이러한 종류의 코드는 기술적으로 암시적 변환이 아니지만, c++언어는 explicit 키워드가 있는 한 이를 암시적 변환으로 취급한다.

 

장점:

  • 암시적 변환은 형식이 분명한 경우 명시적으로 형식을 지정할 필요가 없도록 하여 형식을 보다 유용하고 표현력 있게 만들 수 있다.
  • 암시적 변환은 예를들어 string_view 매개변수가 있는 단일 함수가 std::string 및 const char*에 대한 개별 오버로드를 대신하는 경우와 같이 오버로딩에 대한 더 간단한 대안이 될 수 있다.
  • List 초기화 구문은 객체를 초기화하는 간결하고 표현력있는 방법이다.
여기서 string_veiw란?
c++17에 추가된 문법으로 다양한 문자열 타입을 전달 받을 수 있는 안전 하면서 효과적인 방법
https://jungwoong.tistory.com/56

 

단점:

  • 암시적 변환은 대상 유형(destination type)이 사용자의 기대와 일치하지 않거나, 사용자가 변환이 발생할 것이라는 사실을 모르는 경우 유형 불일치 버그를 숨길 수 있다.
  • 암시적 변환은 특히 오버로딩이 있는 경우 실제로 호출되는 코드가 무엇인지 덜 명확하게 하여 코드를 읽기 어렵게 만들 수 있다.
  • 단일 인수를 사용하는 생성자는 의도하지 않은 경우에도 실수로 암시적 형식 변환으로 사용할 수 있다.
  • 단일 인수 생성자가 explicit 키워드로 표시되지 않으면 암시적 변환을 정의하기 위한 것인지 작성자가 단순히 표시하는 것을 잊었는지 여부를 알 수 있는 신뢰할 수 있는 방법이 없다.
  • 암시적 변환은 특히 양방향 암시적 변환이 있는 경우 호출 사이트 모호성(call-site ambiguities)을 유발할 수 있다. 이는 암시적 변환을 모두 제공하는 두 가지 형식이 있거나 암시적 생성자와 암시적 형식 변환 연산자가 모두 있는 단일 형식으로 인해 발생할 수 있다.
  • List 초기화는 대상 유형이 암시적이면, 특히 List에 단일 요소만 있는 경우 동일한 문제를 겪을 수 있다.

 

결정:

형식 변환 연산자 및 단일 인수로 호출 가능한 생성자클래스 정의에서 explicit 키워드로 표시되어야 한다. 예외적으로 복사 및 이동 생성자는 형식 변환을 수행하지 않으므로 explicit으로 설정되서는 안 된다.

 

암시적 변환은 예를 들어 두 유형의 객체가 동일한 기본 값의 다른 형식일 때와 같이 상호 교환 가능하도록 설계된 형식에 대해 필요하고 적절할 수 있다. 이 경우 당신의 프로젝트 리드에게 연락하여 이 규칙의 포기를 요청해라.

 

단일 인수로 호출할 수 없는 생성자는 explicit을 생략할 수 있다. 단일 std::initializer_list 매개변수를 사용하는 생성자는 복사 초기화를 지원하기 위해 explicit도 생략해야 한다(예: MyType m = {1, 2};).


복사 가능 및 이동 가능 유형(Copyable and Movable Types)

클래스의 public API는 클래스가 복사 가능한지, 이동만 가능한지, 복사하거나 이동할 수 없는지 여부를 분명히 해야 한다. 이러한 작업이 당신의 유형에 대해 명확하고 의미가 있는 경우 복사 및/또는 이동을 지원한다.

 

정의:

이동 가능한 유형은 임시(temporary)에서 초기화하고 할당할 수 있는 유형이다.

A temporary is an unnamed object (the results of some expressions), and is always an rvalue. Or perhaps one should better say that an expression which results in an rvalue is a temporary.
https://stackoverflow.com/questions/15130338/what-are-c-temporaries

복사 가능한 유형은 소스의 값이 변경되지 않는다는 조건으로 동일한 유형(정의에 따라 이동도 가능함)의 어떤 다른 객체에서 초기화되거나 할당될 수 있는 유형이다.

std::unique_ptr<int> 이동할 수 있지만 복사할 수 없는 유형의 예이다(소스 std::unique_ptr<int>의 값은 대상에 할당하는 동안 수정되어야 하기 때문). int 및 std::string복사 가능한, 이동 가능한 유형의 예이다. (int의 경우 이동 및 복사 작업이 동일하고, std::string의 경우 복사보다 저렴한 이동 작업이 있다.)

std::move를 통해 데이터를 이동시킬 수 있다.
### c++
void f()
{
    unique_ptr<int> a(new int(3));
    unique_ptr<int> b = move(a);
    cout << *b.get() << endl;
    //cout << *a.get() << endl; //Segmentation Fault!
}
move를 호출하면 a가 소유하고 있는 객체(new int(3))는 b에게 넘어간다. a는 empty 상태가 되고 a를 통한 접근은 segmentation fault를 발생시킨다.

### c++
unique_ptr<int> a = unique_ptr<int>(new int(3));
unique_ptr<int> b = a; //error!!
위와 같이 대입하는 것도 불가능하다.

https://bunhere.tistory.com/407

사용자 정의 형식의 경우, 복사 동작은 복사 생성자와 복사 할당 연산자에 의해 정의된다. 이동 동작은 이동 생성자와 이동 할당 연산자(있는 경우) 또는 복사 생성자와 복사 할당 연산자(있는 경우)에 의해 정의된다.

 

복사/이동 생성자는 일부 상황(예: 값으로 객체를 전달할 때)에서 컴파일러에 의해 암시적으로 호출될 수 있다.

 

장점:

복사 가능 및 이동 가능 유형의 객체는 값으로 전달 및 반환될 수 있으므로, API가 더 간단하고 안전하며 더 일반적이다. 포인터나 참조로 객체를 전달할 때와 달리 소유권, 수명, 변경 가능성 및 유사한 문제에 대한 혼동의 위험이 없으며 계약에 명시할 필요도 없다. 또한 클라이언트와 구현 간의 비-로컬 상호 작용을 방지하여 컴파일러에 의한 이해, 유지 관리 및 최적화하기가 더 쉽다. 또한 이러한 객체는 대부분의 컨테이너와 같이 값에 의한 전달(pass-by-value)이 필요한 일반 API와 함께 사용할 수 있으며, 예를 들어 유형 구성에서 추가적인 유연성을 허용한다.

 

복사/이동 생성자 및 할당 연산자는 일반적으로 Clone(), CopyFrom() 또는 Swap()과 같은 대안보다 올바르게 정의하기가 쉽다. 암시적으로 또는 = default를 사용하여 컴파일러에서 생성할 수 있기 때문이다. 복사/이동 생성자 및 할당 연산자는 간결하고 모든 데이터 멤버가 복사되도록 보장한다. 복사 및 이동 생성자는 힙 할당이나 별도의 초기화 및 할당 단계가 필요하지 않고, 복사 제거(copy elision)와 같은 최적화에 적합하기 때문에 일반적으로 더 효율적이다.

 

이동 작업을 사용하면 rvalue 객체에서 리소스를 암시적이고 효율적으로 전송할 수 있다. 이것은 어떤 경우에는 더 단순한 코딩 스타일을 허용한다.

 

단점:

일부 유형은 복사 가능하지 않아도 되며 이러한 유형에 대한 복사 작업을 제공하는 것은 혼란스럽거나 무의미하거나 완전히 올바르지 않을 수 있다. 싱글톤 객체(Registerer), 특정 범위에 묶인 객체(Cleanup) 또는 객체 ID(object identity)에 밀접하게 결합된 객체(Mutex)를 나타내는 형식은 의미 있게 복사할 수 없다. 다형성으로 사용될 기본 클래스 유형에 대한 복사 작업은 객체 슬라이싱(object slicing)으로 이어질 수 있기 때문에 위험하다. 기본값 또는 부주의하게 구현된 복사 작업은 올바르지 않을 수 있으며 결과 버그는 혼란스럽고 진단하기 어려울 수 있다.

 

object identity
객체는 변수의 값 또는 메서드의 정의의 일부 또는 전부가 시간이 지남에 따라 변경되더라도 자신의 정체성을 유지합니다.
https://www2.cs.sfu.ca/CourseCentral/354/zaiane/material/notes/Chapter8/node8.html

 

복사 생성자는 암시적으로 호출되므로 호출을 놓치기 쉽다. 이것은 참조에 의한 전달이 관습적이거나 필수인 언어에 익숙한 프로그래머에게 혼란을 일으킬 수 있다. 또한 과도한 복사를 조장하여 성능 문제를 일으킬 수 있다.

 

결정:

모든 클래스의 공용 인터페이스는 클래스가 지원하는 복사 및 이동 작업을 명확히 해야 한다. 이것은 일반적으로 선언의 public 섹션에서 적절한 작업을 명시적으로 선언 및/또는 삭제하는 형식을 취해야 한다.

 

특히 복사 가능한 클래스는 복사 작업을 명시적으로 선언해야 하고, 이동 전용 클래스는 이동 작업을 명시적으로 선언해야 하며, 복사 불가능/이동 가능한 클래스는 복사 작업을 명시적으로 삭제해야 한다. 복사 가능한 클래스는 효율적인 이동을 지원하기 위해 이동 작업을 선언할 수도 있다. 네 가지 복사/이동 작업을 모두 명시적으로 선언하거나 삭제할 수 있지만 필수는 아니다. 복사 또는 이동 할당 연산자를 제공하는 경우 해당 생성자도 제공해야 한다.

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;
};

이러한 선언/삭제는 명백한 경우에만 생략할 수 있다:

  • 클래스에 구조체 또는 인터페이스 전용 base 클래스와 같은 private 섹션이 없는 경우 복사 가능성/이동성은 어떤 public 데이터 멤버의 복사 가능성/이동성에 의해 결정될 수 있다.
  • Base 클래스가 분명히 복사 불가능하거나 이동할 수 없다면, 파생 클래스도 당연히 복사할 수 없다. 이러한 작업들을 암시적으로 남겨두는 인터페이스 전용 기본 클래스는 구체적인 하위 클래스를 명확하게 만드는 데 충분하지 않다.
  • 복사에 대한 생성자 또는 할당 작업을 명시적으로 선언하거나 삭제하는 경우 다른 복사 작업은 명확하지 않으므로 선언하거나 삭제해야 한다. 이동 작업도 마찬가지이다.

 

복사/이동의 의미가 일반 사용자에게 명확하지 않거나 예상치 못한 비용이 발생하는 경우 유형을 복사 가능/이동 가능해서는 안된다. 복사 가능한 유형에 대한 이동 작업은 엄밀히 말하면 성능 최적화이며 버그 및 복잡성의 잠재적인 원인이므로 해당 복사 작업보다 훨씬 더 효율적이지 않는 한 정의하지 마라. 유형이 복사 작업을 제공하는 경우 해당 작업의 기본 구현이 정확하도록 클래스를 디자인하는 것이 좋다. 다른 코드와 마찬가지로 기본 작업의 정확성을 검토해야 한다.

 

슬라이싱의 위험을 제거하려면 그들의 생성자들을 protected로 만들거나, 그들의 소멸자를 protected로 선언하거나, 하나 이상의 순수 가상 멤버 함수를 제공하여 기본 클래스를 추상화하는 것을 선호해라. 구체적인 클래스에서 파생되는 것을 피하는 것이 좋다.


구조체 대 클래스(Structs vs. Classes)

데이터를 전달하는 수동 객체(passive object)에만 struct를 사용해라; 다른 모든 것은 class를 사용해라.

passive object 란?
다른 객체와 상호작용하지 않는 객체
https://stackoverflow.com/questions/30745753/passive-objects-in-c

struct 및 class 키워드는 C++에서 거의 동일하게 작동한다. 각 키워드에 고유한 의미를 추가하므로 정의하는 데이터 유형에 적절한 키워드를 사용해야 한다.

 

구조체는 데이터를 전달하고 연결된 상수들을 가질 수 있는 수동 객체에 사용해야 한다. 모든 필드는 public이어야 한다. 구조체에는 다른 필드 간의 관계를 암시하는 불변 속성(invariants)이 없어야 한다. 해당 필드에 대한 직접 사용자 액세스는 해당 불변 속성을 깨뜨릴 수 있기 때문이다. 생성자, 소멸자 및 도우미 메서드가 있을 수 있다; 그러나 이러한 방법은 어떠한 불변 속성도 요구하거나 강제해서는 안 된다.

어떤 객체의 상태가 프로그래머의 의도에 맞게 잘 정의되어 있다고 판단할 수 있는 기준을 제공하는 속성을 가리켜
불변 속성(invariant)이라고 한다.
https://banaba.tistory.com/34

만약 더 많은 기능이나 불변 속성이 필요한 경우 클래스가 더 적합하다. 의심스러운 경우 클래스로 만들어라. 

 

STL과의 일관성을 위해, 상태 비저장(stateless) 유형들을 위해 구조체 대신 클래스를 사용할 수 있는데, 예를 들어 특성(traits), 템플릿 메타 함수(template metafunctions) 및 일부 펑터(functors)가 있다.

traits : https://ence2.github.io/2021/04/c-template-programming-type-traits5/
functor : 함수처럼 호출 가능한 클래스 객체 (function object) : https://pangtrue.tistory.com/19

구조체와 클래스의 멤버 변수에는 다른 명명 규칙이 있다.


구조체 대 쌍 및 튜플(Structs vs. Pairs and Tuples)

요소들이 의미 있는 이름을 가질 수 있을 때마다 쌍(pair)이나 tuple(튜플) 대신 구조체를 사용하는 것을 선호한다.

 

쌍과 튜플을 사용하면 사용자 정의 유형을 정의할 필요가 없어 잠재적으로 코드를 작성할 때 작업을 절약할 수 있지만 의미 있는 필드 이름은 .first, .second 또는 std::get<X>보다 코드를 읽을 때 거의 항상 훨씬 더 명확하다. C++14에서 std::get<Type>을 도입하여 인덱스가 아닌 유형별로 튜플 요소에 액세스 하면(유형이 고유한 경우) 때때로 이를 부분적으로 완화할 수 있지만, 일반적으로 필드 이름은 유형보다 훨씬 더 명확하고 더 유익하다.

 

쌍 및 튜플은 쌍 또는 튜플의 요소에 대한 특정 의미가 없는 일반 코드에서 적절할 수 있다. 기존 코드 또는 API와 상호 운용하기 위해 사용이 필요할 수도 있다.


상속(Inheritance)

종종 구성(composition)이 상속보다 더 적절하다. 상속을 사용하는 경우 public으로 만들어야 한다.

 

정의:

하위 클래스가 기본 클래스를 상속하면 기본 클래스가 정의하는 모든 데이터 및 작업의 정의가 포함된다. "인터페이스 상속"은 순수한 추상 기본 클래스(상태나 정의된 메서드가 없는 클래스)로 부터의 상속이다. 다른 모든 상속은 "구현 상속"이다.

 

장점:

구현 상속은 기존 유형을 전문화하므로 기본 클래스 코드를 재사용하여 코드 크기를 줄인다. 상속은 컴파일 타임 선언이므로 사용자와 컴파일러는 작업을 이해하고 오류를 감지할 수 있다. 인터페이스 상속을 사용하여 클래스가 특정 API를 노출하도록 프로그래밍 방식으로 적용할 수 있다. 다시 말하지만, 컴파일러는 이 경우 클래스가 API의 필요한 메서드를 정의하지 않을 때 오류를 감지할 수 있다.

 

단점:

구현 상속의 경우 하위 클래스를 구현하는 코드가 기본 클래스와 하위 클래스 사이에 퍼져있기 때문에 구현을 이해하기가 더 어려울 수 있다. 하위 클래스는 가상이 아닌 함수를 재정의할 수 없으므로 하위 클래스는 구현을 변경할 수 없다.

 

다중 상속은 종종 더 높은 성능 오버헤드를 초래하고(실제로 단일 상속에서 다중 상속으로 성능 저하가 일반 디스패치에서 가상 디스패치로 성능 저하보다 더 클 수 있음) "다이아몬드" 상속 패턴으로 이어질 위험이 있기 때문에 특히 문제가 많다. 다이아몬드 상속 패턴은 심각성, 혼란, 그리고 노골적인 버그가 발생하기 쉽다.

 

결정:

모든 상속은 public으로 선언되어야 한다. private 상속을 수행하려면 기본 클래스의 인스턴스를 구성원으로 대신 포함해야 한다. 기본 클래스로 사용하는 것을 지원하지 않을 경우 final 키워드를 클래스 앞에 선언해주면 된다.

구현 상속을 과도하게 사용하지 마라. 컴포지션이 종종 더 적절하다. 상속의 사용을 "is-a"의 경우로 제한한다: "Bar가 Foo의 한 종류"라고 합리적으로 말할 수 있다면 Bar는 Foo를 상속한다.

하위 클래스에서 액세스해야 할 수 있는 멤버 함수로 protected의 사용을 제한합니다. 데이터 구성원은 private여야 합니다.

override 키워드 또는 final 키워드(빈도가 낮은) 중 하나를 사용하여 가상 함수 또는 가상 소멸자의 override(재정의)에 명시적으로 주석을 달 수 있다. 재정의를 선언할 때 virtual 키워드를 사용하지 마라.

근거: 기본 클래스 가상 함수의 재정의가 아닌 "override" 또는 "final"으로 표시된 함수 또는 소멸자는 컴파일되지 않으며, 이는 일반적인 오류를 잡는 데 도움이 된다. 지정자가 없는 경우, 독자는 해당 클래스의 모든 상위 항목을 확인하여 함수 또는 소멸자가 가상인지 여부를 확인해야 합니다.

다중 상속은 허용되지만 다중 구현 상속은 권장되지 않는다.


연산자 오버로딩(Operator Overloading)

연산자를 신중하게 오버로드하라. 사용자 정의 리터럴(user defined literal)을 사용하지 마라.

UDL(user defined literal) 
http://egloos.zum.com/sweeper/v/3147895

정의:

C++에서는 매개변수 중 하나가 사용자 정의 유형인 한 사용자 코드에서 operator 키워드를 사용하여 내장 연산자의 오버로드된 버전을 선언할 수 있다. operator 키워드는 또한 사용자 코드가 operator""를 사용하여 새로운 종류의 리터럴을 정의하고 operator bool()과 같은 유형 변환 함수를 정의할 수 있도록 한다.

 

장점:

연산자 오버로딩은 사용자 정의 유형이 기본 제공 유형과 동일하게 작동하도록 하여 코드를 보다 간결하고 직관적으로 만들 수 있다. 오버로드된 연산자는 특정 작업(예: ==, <, = 및 <<)에 대한 관용적 이름이며 이러한 규칙을 준수하면 사용자 정의 유형을 더 읽기 쉽게 만들고 해당 이름을 필요로 하는 라이브러리와 상호 운용할 수 있다.

 

사용자 정의 리터럴은 사용자 정의 유형의 객체를 생성하기 위한 매우 간결한 표기법이다.

 

단점:

  • 정확하고 일관되며 놀라운 연산자 오버로드 집합을 제공하려면 약간의 주의가 필요하며 그렇게 하지 않으면 혼란과 버그가 발생할 수 있다.
  • 연산자를 남용하면 특히 오버로드된 연산자의 의미가 규칙을 따르지 않는 경우 코드가 난독화될 수 있다.
  • 함수 오버로딩의 위험은 연산자 오버로딩과 마찬가지로 적용된다.
  • 연산자 오버로드는 우리의 직관을 속여서 값비싼 연산이 저렴한 내장된 연산이라고 생각하도록 속일 수 있다.
  • 오버로드된 연산자에 대한 호출 사이트를 찾으려면 예를 들어 grep이 아닌 C++ 구문을 인식하는 검색 도구가 필요할 수 있다.
  • 오버로드된 연산자의 인수 유형이 잘못되면 컴파일러 오류가 아닌 다른 오버로드가 발생할 수 있다. 예를 들어, foo < bar는 한 가지 일을 하는 반면 &foo < &bar는 완전히 다른 일을 한다.
  • 특정 작업자 과부하는 본질적으로 위험하다. 단항 &을 오버로드하면 오버로드 선언이 표시되는지 여부에 따라 동일한 코드가 다른 의미를 가질 수 있다. &&, || 및 ,(쉼표)의 오버로드는 기본 제공 연산자의 평가 순서 의미 체계와 일치할 수 없다.
  • 연산자는 종종 클래스 외부에서 정의되므로 동일한 연산자에 대해 다른 정의를 도입하는 다른 파일의 위험이 있다. 두 정의가 동일한 바이너리에 연결되면 정의되지 않은 동작이 발생하여 미묘한 런타임 버그로 나타날 수 있다.
  • 사용자 정의 리터럴(UDLs)을 사용하면 std::string_view("Hello World")의 약어인 "Hello World"sv와 같이 숙련된 C++ 프로그래머에게도 익숙하지 않은 새로운 구문 형식을 만들 수 있다. 기존 표기법은 덜 간결하지만 더 명확하다.
  • 그것들은 네임스페이스로 한정될 수 없기 때문에 UDL을 사용하려면 using 지시문(우리는 금지) 또는 using 선언(가져온 이름이 문제의 헤더 파일). 헤더 파일이 UDL 접미사를 피해야 한다는 점을 감안할 때 헤더 파일과 소스 파일 간에 리터럴에 대한 규칙이 다른 것을 피하는 것이 좋다.

결정:

오버로드 연산자는 의미가 분명하고 놀랍지 않으며 해당 기본 제공 연산자와 일치하는 경우에만 정의해라. 예를 들어 |을 쉘 스타일 파이프가 아닌 비트 또는 논리적 파이프로 사용한다.

 

사용자 유형에 대해서만 연산자를 정의한다. 보다 정확하게는 운영 유형과 동일한 헤더, .cc파일 및 네임스페이스로 정의해라. 이렇게 하면 연산자는 유형이 어디에 있든 사용할 수 있으므로 다중 정의의 위험을 최소화할 수 있다. 연산자는 가능한 템플릿 인수에 대해 이 규칙을 충족해야 하므로 가능한 경우 템플릿으로 정의하지 마라. 연산자를 정의하는 경우 의미 있는 관련 연산자도 정의하고 연산자를 일관되게 정의해야 한다. 예를 들어, 오버로드 < 하는 경우 모든 비교 연산자를 오버로드하고 동일한 인수에 대해 < 및 > 가 true를 반환하지 않도록 한다.

 

수정되지 않는 이항 연산자를 비 구성원 함수로 정의하는 것을 선호한다. 이진 연산자가 클래스 멤버로 정의되면 암묵적 변환은 오른쪽 인수에 적용되지만 왼쪽 인수는 적용되지 않는다. 만약 a < b는 컴파일되지만 b < a는 컴파일 되지 않는다면 그것은 당신의 사용자들을 혼란스럽게 한다.

 

연산자 과부하를 정의하지 않기 위해 무리하지 마라. 예를 들어, Equals(), CopyFrom(), PrintTo()보다는 ==, = 및 <<를 정의하는 것을 선호한다. 반대로, 다른 라이브러리가 예상한다고 해서 연산자 오버로드를 정의하지 마라. 예를 들어, 유형에 자연 순서가 없지만 std::set으로 저장하려면 < 을(를) 오버로드 하지 말고 사용자 정의 비교기를 사용해라.

 

&, ||, ,(쉼표) 또는 단항 &를 오버로드 하지 마라. operator""를 오버로드 하지 마라. 즉, 사용자 정의 리터럴을 도입하지 마라. 다른 사람이 제공한 리터럴(표준 라이브러리 포함)을 사용하지 마라.

 

형식 변환 연산자는 암시적 변환에 대한 절에서 다룬다. = 오퍼레이터는 복사 생성자 섹션에서 다룬다. 스트림과 함께 사용하기 위한 << 오버로드에 대해서는 스트림 절에서 다룬다. 연산자 오버로드에도 적용되는 함수 오버로드 규칙을 참조하라.


접근 제어(Access Control)

상수가 아닌 경우 클래스의 데이터 멤버를 private로 설정하라. 이것은 필요한 경우 접근자(일반적으로 const) 형태의 쉬운 상용구를 희생하여 불변 속성에 대한 추론을 단순화한다.

 

기술적인 이유로 Google Test를 사용할 때 .cc 파일에 정의된 test fixture 클래스의 데이터 멤버를 protected로 선언하는 것을 허용한다. test fixture 클래스가 .cc 파일 외부에 정의되어 있는 경우(예: .h 파일에서) 데이터 멤버를 private로 설정한다.


선언 순서(Declaration Order)

유사한 선언을 함께 그룹화하여 public 부분을 더 일찍 배치한다.

 

클래스 정의는 일반적으로 public: 섹션으로 시작하고 그다음에 protected:, private: 가 따라와야 한다. 비어 있는 섹션은 생략한다.

 

각 섹션 내에서 유사한 종류의 선언을 함께 그룹화하는 것을 선호하고 다음 순서를 선호한다.

  1. 유형 및 유형 별칭(typedef, using, enum, 중첩 구조체 및 클래스)
  2. 정적 상수들
  3. 팩토리 함수들
  4. 생성자와 할당 연산자
  5. 소멸자
  6. 기타 모든 함수(정적 및 비정적 멤버 함수, friend 함수)
  7. 데이터 멤버(정적 및 비정적)

클래스 정의에 큰 메서드 정의를 인라인으로 두지 마라. 일반적으로 사소하거나 성능이 중요한 매우 짧은 메서드만 인라인으로 정의할 수 있다. 자세한 내용은 인라인 함수(Inline Functions)를 참조해라.

300x250

댓글