본문 바로가기
Language/C&C++

[c++] C++ Core Guidelines

by 긞 2019. 11. 6.

[[[내용 가운데 수정이 필요한 부분을 발견하시면 댓글로 알려주세요]]]

C++ Core Guidelines

In: 소개

P: 철학

I: 인터페이스

I.1: 명시적인 인터페이스를 만드세요.

이유 인터페이스에 명시되지 않은 가정은 간과하기 쉽고, 검증이 어렵습니다.

나쁜 예제 글로벌 변수를 통한 함수 제어는 암시적이고 혼란을 일을킬 수 있습니다. 예를 들면:


int round(double d)
{
    return (round_up) ? ceil(d) : d;    // 보이지 않는 의존을 사용하지 마십시오.
}

round(7.2) 함수를 호출했을 때 결과 값이 다를 수 있어 함수 호출자에게는 의미가 분명하지 않을 수 있다. [^^: round_up 변수를 말하는 것이 아닐까?]

예외 가끔 환경 변수(예, 일반적인 출력 vs 상세한 출력, 또는 디버그 vs 최적화)로 세부 동작을 제어합니다. 비지역제어 사용은 혼란을 야기시킬 수 있습니다. 고정된 의미론의 구현 세부사항만 제어합니다.

나쁜 예제 비지역 변수를 통한 보고는 쉽게 무시됩니다. 예를 들면:


    // printf의 반환 값을 검증하지 마세요.
    fprint(connection, "logging: %d %d %d\n", x, y, x);

연결 해제로 더이상 처리할 수 없게 된다면?

대안: 예외를 던지세요. 예외는 무시될 수 없습니다.

대안 공식: 인터페이스로 비지역 또는 암시적 상태 값을 통한 정보 전달을 피하세요. 비상수(non-const) 멤버 함수는 오브젝트의 상태로 다른 함수에 정보를 전달한다는 것을 명심하세요.

대안 공식: 인터페이스는 함수 또는 함수들의 묶음이여야 합니다. 함수들은 템플릿 함수가 될 수 있고, 함수 묶음은 클래스 또는 클래스 템플릿이 될 수 있습니다.

강조

  • 이름공간(namespace) 구역에 정의된 변수의 값을 근거로 함수가 제어 흐름을 결정해서는 안됩니다.
  • 함수가 이름공간 구역에 정의된 변수에 값을 설정하면 안됩니다.

I.2: 비상수(non-const) 전역 변수 사용을 피하세요.

이유 비상수 전역 변수는 종속성을 숨기고 종속성을 예상할 수 없게 만듭니다.

예제


struct Data {
    //   ... 생략 ...
} data;     // 비상수 데이터

void compute()
{
    // data 사용
}

void output()
{
    // data 사용
}

누가 데이터를 수정할 수 있을까요?

참고 전역 상수가 유용합니다.

참고 전역 변수에 대한 규칙은 이름공간 범위 변수에도 적용됩니다.

대안: 복사를 피하기 위해 전역 데이터를 사용한다면, const 참조를 통해 데이터를 객체로 전달하는 것이 좋습니다. 다른 해결책은 데이터를 일부 객체의 상태로 정의하고 기능을 멤버 함수로 정의하는 것입니다.

주의: 데이터 경쟁(data races)를 주의하세요. 만약 한 스레드가 다른 스레드가 수신자를 수행할 때 비지역 데이터에 접근(또는 참조로 데이터를 전달)할 수 있다면, 데이터 경쟁이 발생할 수 있습니다. 가변 데이터에 대한 모든 포인터나 참조는 데이터 경쟁이 일어날 수 있습니다.

참고 불변 데이터는 경쟁 조건이 없습니다.

참조: 함수 호출 규칙을 참조하세요.

참고 이 규칙은 "피하라"이지, "사용 금지"가 아닙니다. 물론 cin, cout, 그리고 cerr과 같은 ( 드문) 예외들이 있습니다.

강조 이름영역 범위에 선언된 모든 비상수 변수를 보고하세요.

I.3: 싱글톤(singleton)을 피하세요.

이유 싱글톤은 기본적으로 가장 복잡한 전역 객체입니다.

예제


class Singleton {
    //  단 하나의 싱글톤 객체 생성을 확인하기 위한 많은 작업들...
    // 적철하게 초기화가 되었다.
};

싱글톤에는 수 많은 변형된 형태가 있습니다. 이것도 문제 가운데 하나입니다.

참고 전역 객체를 변경하지 않으려면, constconstexpr로 선언하세요.

예외 가장 단순한 (아주 단순해서 종종 싱글톤으로 간주되지 않는) "싱글톤"을 사용하여 처음 사용할 때 초기화할 수 있습니다.


X& myX()
{
    static X my_x {3};
    return my_x;
}

이는 초기화 순서와 관련된 문제에 대한 가장 효과적인 해법 가운데 하나입니다. 다중 스레드 환경에서 정적 객체 초기화는 (생성자 내에서 공유 객체에 부주의하게 접근하지 않는 한) 경쟁 조건이 발생하지 않습니다.

지역 정적(local static) 초기화는 경쟁 조건을 의미하지 않습니다. 그러나 X의 소멸이 동기화가 필요한 연산자를 포함한다면 조금 더 덜 간단한 해법을 사용해야 합니다. 예를 들어:


X& myX()
{
    static auto p = new X{3};
    return *p;  // 잠재적 누수
}

누군가는 적절한 스레드 안전(thread-safe) 방법으로 이 객체를 삭제(delete)해야 합니다. 오류가 발생하기 쉽기 때문에 이 기술을 사용하지 않습니다.

  • myX는 다중 스레드 코드에서,
  • X 객체의 소멸이 필요할 때(예: 리소스를 해제하기 때문에),
  • X의 소멸자 코드의 동기화가 필요할 때.

많은 사람들이 단일 객체를 하나의 객체만 만드는 클래스로 정의하는 경우, myX와 같은 함수는 싱글톤이 아니며, 이 유용한 기술은 무-싱글톤 규칙의 예외가 아닙니다.(??)

강조 일반적으로 매우 어렵습니다.

  • 싱글톤(sigleton)을 포함하는 이름을 가진 클래스를 찾으십시오.
  • (객체를 계산하거나 생성자를 검사하여) 단일 객체만 생성되는 클래스를 찾습니다.
  • 클래스 X에 클래스 유형 X의 함수

I.4: 인터페이스를 정확하고 강력하게 형식화(typed)하세요.

이유 형식화는 가장 단순하면서 최고의 문서입니다. 의미를 잘 정의하면 가독성이 향상되고 컴파일 시점에 확인이 됩니다. 또한 정확하게 형식화된 코드는 최적화가 잘됩니다.

하면 안되는 예제 고려하세요:


void pass(void* data);  // 약하고 자격이 없는 형은 의심스럽습니다.

호출자는 어떤 형들이 허용되는지 또는 const가 지정되지 않아 데이터가 변경될 수 있는지 확신할 수 없습니다. 모든 포인터 형은 암시적으로 void* 로 변환되므로, 호출자가 값을 쉽게 제공할 수 있습니다.

호출자는 미확인 형을 사용할 때 데이터를 반드시 static_cast 해야합니다.

대안: 템플릿 매개 변수는 void를 제거하여 T\ 또는 T&로 바꿀 수 있습니다. 일반 코드의 경우 T는 일반 또는 개념 제한 템플릿 매개 변수일 수 있습니다.

나쁜 예제 고려하세요:


draw_rect(100, 200, 100, 500);  // 숫자들은 무엇을 지정합니까?

draw_rect(p.x, p.y, 10, 20);    // 10과 20의 단위는 무엇입니까?

호출자가 직사각형을 나타내는 것은 분명하지만, 무엇과 관련되어 있는지는 불분명합니다. 또한 int는 다양한 단위의 값을 포함해서 임의의 형태의 정보를 전달할 수 있습니다. 따라서 네 개의 int의 의미를 추측해야 합니다. 아마도 앞에 두 개는 x와 y, 좌표 쌍이지만 나머지 두 개는 무엇일까요?

주석과 매개 변수 이름이 도움을 줄 수 있지만 명확해야 합니다:


void draw_rectangle(Point top_left, Point bottom_right);
void draw_rectangle(Point top_left, Size height_width);

draw_rectangle(p, Point{10, 20});   // 꼭지점 둘
draw_rectangle(p, Size{10, 20});    // 꼭지점 하나와 (높이, 너비)쌍

솔직히, 정적 타입 시스템(static type system)을 통해 모든 오류를 잡아낼 수 없습니다.
(예를 들어, 첫 번째 인자가 상단-왼쪽 위치일 것이라는 것은 (작명과 주석) 관례이다.

나쁜 예제 고려하세요:


set_settings(true, false, 42);  // 숫자가 명시하는 것은 무엇일까요?

매개 변수 타입과 값은 설정 값이 무엇을 명시하는지, 값의 의미는 무엇인지 서로 소통하지 않습니다.

이 디자인이 보다 명확하고 안전하고 읽기 쉽습니다:


alarm_settings s{};
s.enabled = true;
s.displayMode = alarm_settings::mode::spinning_light;
s.frequency = alarm_settings::every_10_seconds;
set_settings(s);

boolean 값을 설정할 경우 열거형(enum) 플래그(boolean 값 집합을 표현하는 패턴)을 사용하는 것이 좋습니다.


enable_lamp_options(lamp_option::on | lamp_option::animation_state_transitions);

나쁜 예제 다음 예제에서 인터페이스에서 time_to_blink가 어떤 의미인지 명확하지 않습니다: 초? 밀리 초?


void blink_led(int time_to_blink)   //나쁨 -- 단위가 모호합니다.
{
    // ...
    // time_to_blink로 처리
    // ...
}

void use()
{
    blink_led(2);
}

좋은 예제 std::chrono::duration 타입(C++11)이 기간의 단위를 명확하게 하도록 도와줍니다.


void blink_led(miiliseconds time_to_blink)  // 좋음 -- 단위가 명확합니다.
{
    // ...
    // time_to_blink로 처리
    // ...
}

void use()
{
    blink_led(1500ms);
}

이 함수는 모든 시간 단위를 허용하는 방식으로 작성할 수도 있습니다.


template
void blink_use(duration time_to_blink) // 좋음 -- 모든 단위를 받습니다.
{
    // 밀리 초가 가장 작은 단위라고 가정합니다.
    auto milliseconds_to_blink = duration_cast(time_to_blink);
    // ...
    // *milliseconds_to_blink*로 처리
    // ...
}

void use()
{
    blink_led(2s);
    blink_led(1500ms);
};

강조

  • (단순) void*를 매개 변수 또는 반환 타입으로 사용하는 것을 보고하십시오.
  • (단순) 하나 이상의 bool 매개 변수를 사용하는 것을 보고하십시오.
  • (어려움) 많은 기본 유형 인수를 사용하는 함수를 찾으십시오.

I.22: 전역객체의 복잡한 초기화를 피하십시오.

이유 복잡한 초기화는 정의되지 않은 실행 순서로 이어질 수 있습니다.

예제


// file1.c
extern const X x;

const Y y = f(x);   // read x; write y

// file2.c

extern const Y y;

const X x = g(y);   // read y; write x

xy는 다른 번역 단위이기 때문에 f()와 g()를 호출하는 순사가 정의되지 않는다. 한 쪽은 초기화되지 않은 상수(const)에 접근하게 될 것 입니다. 이는 전역 (이름공간 구역) 객체의 초기화 순서가 전역 변수에 제한되지 않는 것을 보여줍니다. [^^:번역 단위translation-unit: 컴파일러는 모든 코드를 한 번에 번역하지 않고 단위별로 번역을 실행합니다. 여기서 컴파일러가 문장을 번역하는 단위를 번역 단위라고 함]

참고 초기화 순서 문제는 특히 병행 코드에서 처리하기 힘듭니다. 일반적으로 전역 (이름공간 구역) 객체를 사용하지 않는 것이 가장 좋습니다.

강조

  • non-constexpr 함수를 호출하는 전역 초기화를 표시하시오.
  • extern 객체를 접근하는 전역 초기화를 표시하시오.

F: 함수

C: 클래스와 계층

Enum: 열거

R: 자원 관리

ES: 표현과 서술

Per: 성능

CP: 동시 실행

E: 오류 처리

Con: 정수와 불변

T: 템플릿과 제네릭

프로그래밍

CPL: C-형식 포그래밍

SF: 소스 파일

SL: 표준 라이브러리

'Language > C&C++' 카테고리의 다른 글

[c++] std::map::operator[]  (0) 2019.09.17