Home [C++] emc++ 02: Modern C++
Post
Cancel

[C++] emc++ 02: Modern C++

항목7: 객체 생성시 괄호(())와 중괄호({})를 구분하라


  • C++11 초기값 지정

    • 괄호로 지정
    • 등호로 지정
    • 중괄호로 지정
  • 대입이 항상 일어나지는 않음

    • Widget w1 : 기본 생성자
    • Widget w2 = w1 : 대입 x, 복사 생성자
    • w1 = w2; : 대입 O, 복사 대입 연산자
  • 균일 초기화(uniform initialization)

    • 중괄호 초기화: 어디에서나 사용할 수 있음
    • non-static 멤버의 기본 초기화값 지정 가능
    • 복사할 수 없는 객체 (std::atomic) 초기화 가능 (“=”로는 불가)

균일 초기화 장점

  1. 다양한 문맥에서 사용 가능

  2. 암묵적 좁히기 변환(narrowing conversion) 방지

  • 초기화하려는 객체의 타입으로 온전하게 표현할 수 있음
1
2
3
4
5
  double x,y,z;
  ...
  int sum1 {x+y+z}; // 오류: double들의 합을 int로 표현 못할 가능성
  int sum2 = x+y+z; // ok (타입변환으로 값이 잘려나감)
  int sum3(x+y+z); // ok
  1. 가장 성가신 구문 해석(most vexing parse)에 자유로움
  • “가장 성가신 구문 해석”:
    • 선언으로 해석할 수 있는 것은 항상 선언으로 해석해야 한다”는 C++ 규칙
    • ex)생성자가 호출을 함수선언으로 해석

단점

  • 예상치 못한 행동

    • 중괄호 초기치, std::initializer_list, 생성자 오버로딩.. 사이에서
    • auto, template 에서 연역이 다르게 동작
  • 생성자 오버로딩

    • std::initializer_list 매개변수가 관여하지 않는 한 중괄호의 의미는 같음
    • std::initializer_list 를 받는 오버로딩 생성자가 있으면…
      • 중괄호 초기화 호출은 이 오버로딩을 선택(암묵적 타입변환)
      • 복사 생성, 이동 생성에서도 마찬가지
      • 호출 가능한 생성자가 있어도 호출할 수 없는 현상이 생기기도 함
    • 오버로딩에서 물러나는 경우
      • 중괄호 초기치의 인수 타입들을 std::initializer_list 타입으로 변환하는 방법이 없는 경우 뿐
    • 기본 생성자의 경우
      • Widget w1{};은 기본 생성자가 호출됨
      • Widget w1({});은 중괄호 초기화 생성자가 호출됨
    • std::vector에서 자주 발생하는 문제들임
    • 문제점
      • 기존코드에std::initializer_list 생성자 추가하면 기존코드 오류
  • 일관되게 괄호, 중괄호를 적용해야함

  • 템플릿사용시에도 주의

1
2
3
4
5
6
7
template<typename T, typename... Ts>
void doSomeWork(Ts&&... params) {
  T localObject(std::forward<Ts>(parms)...);
  T localObject{std::forward<Ts>(parms)...};
}

doSomework<std::vector<int>> (10,20); // 괄호식은 요소가 10개인 벡터, 중괄호식은 요소가 2 인 벡터

참고: https://akrzemi1.wordpress.com/2013/06/05/intuitive-interface-part-i/

항목8: nullptr를 선호해라

  • 0, NULL 오버로딩의 문제
    • void f(int)
    • void f(bool)
    • void f(void*)
    • 암묵적 변환의 우선순위가 같음
    • 보통 f(int)를 호출함 (컴파일되지 않을 수도 있음)
    • 포인터 타입과 정수 타입의 오버로딩을 피해야하는 이유

nullptr의 장점

  • 정수타입이 아님
  • 실제 타입은 std::nullptr_t
  • 모든 raw 포인터 타입으로 암묵적 변환
  • 중의성이 없어짐

템플릿에서의 사용

  • 소스코드의 중복을 피하여 템플릿화할 때
    • 0은 항상 int로 해석
    • NULL은 정수 타입으로 해석됨
    • nullptr은 문제없이 포인터로 암묵적 변환이 일어남

항목9: typedef 보다 별칭 선언을 선호해라


  • using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>

  • 함수 별칭이 더 이해하기 쉬움

using FP = void (*)(int, const std::string&) typedef void (*FP)(int, const std::string&)

별칭 템플릿

  • typedef는 템플릿화 불가능하지만, 별칭은 가능
1
2
3
4
template<typenmae T>
using MyAllocList = std::list<T, MyAlloc<T>>;

MyAllocList<Widget> lw;
1
2
3
4
5
6
template<typenmae T>
struct MyAllocList{
  typedef std::list<T, MyAlloc<T>> type;
}

MyAllocList<Widget>::type lw;
  • typedef사용시 또, typename을 붙여야하는 경우도 있음
    • 의존적 타입의 이름앞에는 typename 을 붙여야하는 C++의 규칙
      • MyAllocList::type은 의존적 타입으로 T에 의존함
    • 타입말고 다른 것을 지칭할 가능성이 있기 때문
1
2
3
4
5
template<typenmae T>
class Widget{
  private:
  typename MyAllocList<Widget>::type list;
}
  • 별칭은 그렇지 않음(비의존적 타입)
    • 컴파일러가 Widget 템플릿을 처리하는 과정에서 컴파일러는 이미 그 타입이 이름임을 앎
    • MyAllocateList가 타입 템플릿이므로, MyAllocList 는 이름임

TMP

  • TMP

    • 적절히 T를 사용하여 타입을 변경해야하는 상황이 있음 (const T, T& 등)
  • type_trait: 접미어 type

    • struct: std::변환<T>::type 형태
      • remove_const, remove_reference, add_lvalue_reference
    • 별칭: std::변환_t<T>

항목10: 범위 없는 enum보다 범위 있는 enum을 선호해라


unscope enum

1
2
enum Color {white, red};
auto white = false; // 오류: 이미 white가 선언됨
  • 범위밖에도 영향을 줌
  • tuple의 필드들을 지칭할 때 유용
    • std::get<uiEmail>(uInfo); // uInfo는 튜플타입

scope enum

1
2
3
4
5
enum class Color {white, red};
auto white = false; // Ok
Color c = white; // error
Color c = Color::white // OK
auto c = Color::white // OK
  • 열거자들에 타입이 훨씬 강력하게 적용

    • 암묵적으로 타입변환 x
    • 캐스팅을 명시적으로 해야함
  • 전방선언 가능 (enum이 쓰이기 전에 컴파일러가 그 크기를 앎)

    • C++98은 오직 enum 정의만 지원 -> 컴파일 의존 관계 늘어남
    • 열거자들 지정 x
    • 새 열거자를 지정해도 다시 컴파일 x
1
2
3
4
enum class Status; // 기본 타입은 int
enum class Status2: std::uinte32_t // 명시적으로 지정 가능
enum Color: std::uint8_t; // 범위없는 것도 지원
void continueProcessing(Status s);

항목11: 정의되지 않은 private 함수보다 delete 함수를 선호하라


  • 특정 함수 호출하지 못하게하려면 그냥 함수 선언 x

  • C++이 자동 생성하는 멤버함수

    • 복사생성자, 복사 배정 연산자
  • 삭제된 함수(멤버 함수에만 가능)

    • = delete
    • public으로 선언하는것이 관례
      • 멤버 함수를 사용하려할 때, 그 함수의 접근성을 점검한 후에야 삭제 여부를 점검
      • 함수를 사용할 수 없는 이유가 private 때문이라는 오해의 여지 제거

장점

  • 삭제된 함수는 어떤 방법으로든 사용불가
  • 특정 타입을 받는 함수를 만들 때 유용
    • 오버로딩 함수를 명시적으로 삭제
  • 원치않는 템플릿 인스턴스화 방지
    • 특수한 포인터 템플릿 인스턴스 삭제
    • 이는 private 방식으로 수행하지 못함(다른 접근 수준으로 특수화 불가능)
C++ 특수한 포인터
  • void*: 역참조, 증가, 감소 불가

  • char*: 개별 문자가아닌 C스타일 문자열을 나타냄

1
2
3
4
template<>
void processPointer<void>(void*) = delete;
template<>
void processPointer<char>(char*) = delete;
  • 철저하게 제거
    • const * 버전
    • const volatile * 버전
    • wchar_t, wchar16_t, wchar32_t 버전

    </div> </details>

항목12: 재정의 함수들을 override로 선언


  • 가상함수 재정의:

    • 파생 클래스 함수를 기반 클래스의 인터페이스를 통해서 호출할 수 있게 만드는 메커니즘
  • 오버라이딩 필수조건

    • 기반클래스 함수가 반드시 가상함수
    • 기반, 파생의 함수 이름이 반드시 동일(소멸자 제외)
    • 매개변수 타입들이 동일
    • const 성이 동일
    • 반환 타입과 예외 명세(exception specification)이 호환
  • C++11에서 추가된 조건

    • 멤버 함수들의 참조 한정사(reference qualifier)들이 반드시 동일
참조 한정사(reference qualifier)
  • 멤버 함수를 왼값 or 오른 값에만 사용할 수 있게 제한
1
2
3
4
5
6
7
8
9
10
11
class Widget{
  public:
    void doWork() &;  // *this 가 왼값일 때만
    void doWork() &&; // *this 가 오른값일 때만 적용
}

Widget makeWidget(); // 팩터리 함수(오른값 리턴)
Widget w; // 보통객체 (왼값 리턴)

w.doWorkd()  //왼값용 리턴
makeWidget().doWorkd()  // 오른값용 리턴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Widget {
public:
  using VD = std::vector<double>;

  VD & data() & {
    return values;
  }
  VD && data() && {
    return std::move(values);
  }
private:
  VD values;
}

auto vals1 = w.data(); // data의 왼값 호출, 복사생성
// makeWidget은 팩터리 함수
auto vals2 = makeWidget().data(); // data의 오른값 호출, 이동생성
  • 추가된 두 개의 Contextural keyord
    • 선언 끝에 나올 때에만 예약된 의미를 가짐
    • override
    • final

항목13: iterator 보다는 const_iterator


  • 반복자가 가리키는 것을 수정할 필요가 없을 때 const_iterator 가 바람직함

    • C++98 에서는 이를 잘 활용할 수 없었음
  • C++11의 cbegin(), cend()

    • 삽입 삭제 위치를 지정하는 목적으로 insert, erase는 실제 const_iterator를 사용함
  • 극도로 일반화된 코드

    • 특정 멤버 함수 대신, 그 멤버 함수에 상응하는 비멤버 함수 를 사용 (std::begin, std::end)
    • 템플릿으로 일반화
      • 주의점: C++11에서는 std::cbegin 과 같은 비멤버함수가 없음

항목14: 예외를 방출하지 않을 함수는 noexcept로 선언


방출과 발생, 던지기는 구분되는 개념

  • C++98에서의 예외 명세
    • 함수가 방출할 수 있는 예외 형식들을 요약해야했음
      • 구현 수정 -> 예외 명세 변경 -> 클라이언트 코드 깨질 가능성
  • noexcept: C++11에서 함수 선언 시 그 함수가 예외를 방출하지 않을 것임을 명시

noexcept 함수구현과 예외 명세 사이의 비일관성을 파악하는데 컴파일러가 별 도움을 주지않음 (noexcept 함수안에서 noexcept아닌 함수 호출 가능)

장점

인터페이스 명세의 견고함

  • 함수의 예외 방출 행동 == 클라이언트에게 중요한 사항

컴파일러가 더 나은 목적 코드(Object Code) 산출

  • C++11의 noexcept에서 예외가 나오면, 프로그램 실행이 종료되기 전에 호출 스택이 풀릴 수 도 있고, 풀리지 않을 수도 있음
    • 호출자에게 까지 예외가 전달되는 일이 없음 (어차피 프로그램이 종료)
    • 프로그램 분석시, noexcept 함수 호출들을 고려대상에서 제외 가능
  • C++98에서는 (throw) 예외명세가 위반되면, 호출 스택이 함수를 호출한 지점에 도달할 때까지 풀림

이동 연산에서의 장점

1
2
3
4
std::vector<Widghet> vw;
...
Widget w;
vw.push_back(w);
  • vector의 rsize는 C++98에서 복사였음

    • 강한 예외 안전성 보장 (복사도중 예외 생겨도, vector의 상태는 그대로)
  • C++11에서는 이동으로 최적화

    • 예외 안정성 보장이 위반될 가능성 이있음 (이미 수정된상태)
    • 이동연산이 예외를 방출하지 않음이 확실한 경우에는 복사를 이동으로 대체해도 안전
  • 표준 라이브러리의 여러 함수는 “가능하면 이동하되 필요하면 복사” 라는 전략을 활용함

    • vector::reserve, deque::insert 등
    • 오직 이동 연산이 예외를 방출하지 않음이 알려진 경우에만 C++98의 복사연산을 C++11의 이동연산으로 대체함

SWAP 함수

  • 여러 SWAP 함수들은 noexcept 여부에 의존함

더 중요한 것은 정확성

  • 함수 구현이 예외를 방출하지 않는다는 성질을 오랫동안 유지하는 경우에만 noexcept 사용

  • 대부분 함수는 예외에 중립적(noexcept 불가)

    • 스스로 예외를 던지지 않지만, 예외를 던지는 다른 함수들을 호출할 수 있음
    • 다른 함수가 예외 던지면, 그 예외를 통과시킴
  • 기본적으로 모든 메모리 해제 함수와 모든 소멸자는 암묵적으로 noexcept

    • 소멸자 예외방출 가능성 명시 -> noexcept(false)

넓은 계약과 좁은계약

  • 넓은 계약들을 가진 함수
    • 전제조건이 없는 함수
    • 프로그램의 상태와는 무관하게 호출가능
    • 호출자가 전달하는 인수들에 어떤 제약도 없음
    • 미정의 행동 없음
    • noexcept 선언하는 것 쉬움
  • 좁은 계약들을 가진 함수
    • 넓은 계약들을 가지지 않은 함수
    • 전제조건이 위반되면 그 결과는 미정의 행동
    • 전제조건들이 유효한지 확인하는 것은 호출자 책임
      • noexcept -> 오류검출이 쉽지않음 (던져진 예외를 디버깅할 수 없음)

기억해 둘 사항들

  • noexcept는 함수의 인터페이스 일부, 호출자가 noexcept 여부에 의존할 가능성이 있음
  • noexcept는 최적화 여지가 큼
  • 이동연산, swap, 메모리 해제 함수들, 소멸자들에 특히 유용
  • 대부분 함수는 noexcept가 아니라 예외에 중립적

항목15: 가능하면 항상 constexpr 사용


“constexpr을 객체에 적용 == const의 강화된 버전처럼 작용” but “함수에 적용 == 다른 의미로 작용”

  • constexpr

    • 상수임을 나타내는것
    • 컴파일 시점에서 값이 알려지는 것
      • 읽기 전용 메모리에 배치될 수 있음
      • 정수 상수 표현식이 요구되는 문맥에서 사용할 수 있음 (배열 크기, 템플릿 인수, 열거자 값, alignment)
    • 함수에 적용
      • constexpr의 결과가 반드시 const인 것이 아님, 반드시 컴파일 시점에서 알랴진다는 보장이 없음 (장점임)
  • const

    • const 객체가 반드시 컴파일 시점에서 알려지는 값으로 초기화되지는 않음
1
2
3
4
int sz;

const auto arrsize = sz;
std::array<int, arrsize> data; // 오류

함수에 적용

  • 컴파일 타임 상수가 인수인 경우
    • 컴파일 타임 상수를 산출
  • 런 타임에 결정되는 값이 인수인 경우

    • 런타임 값 산출
  • 두 버전으로 나누어서 구현할 필요가 없음

ex) constexpr pow 함수로, 상수 n이 주어졌을 때 3^n 을 컴파일 타임 때 계산할 수 있음 (이 값을 배열의 크기로 가능)

  • C++11 의 제약

    • 실행 가능 문장이 많아야 하나 (대부분 return문)
      • 요령: 삼항연산자, 재귀
  • C++14 제약

    • C++11 보다 느슨, for문 등 가능
  • 반드시 리터럴 타입(컴파일 시 결정되는 타입)을 받고, 리턴해야함

생성자

  • 생성자 또한 constexpr 가능
  • 객체를 읽기 전용 메모리 안에 생성 가능
  • mid.xValue() * 10 과 같은 표현식을 템플릿 인수나 열거자의 값을 지정하는 표현식에서 사용 가능

  • C++11에서는 setter 함수들을 constexpr로 선언하지 못함 (두가지 이유)

    1. 작동 대상 객체를 수정
    • C++11에서는 constexpr 멤버함수는 암묵적으로 const로 선언됨
    1. 반환 타입이 void
    • C++11에서 void 는 리터럴 타입이 아님
  • C++14에서는 setter 또한 constexpr로 가능

기억해 둘 사항들

  • constexpr 객체는 const, 컴파일 도중 알려지는 값들로 초기화됨
  • constexpr 함수는 그 값이 컴파일 도중에 알려지는 인수들로 호출하는 경우에는 컴파일 시점 결과를 산출
  • constexpr 객체나 함수는 비 constexpr 보다 넓은 문맥에서 사용가능
  • constexpr은 객체나 함수의 인터페이스 일부
    • 함수 또는 객체를 상수표현식을 요구하는 문맥에서 사용 가능하다는 의미
  • constexpr 함수에서는 입출력 문장들이 허용되지 않음

항목16: const 멤버 함수를 스레드에 안전하게 작성하라


  • const 멤버함수 안에 mutable로 선언된 변수가 있을 때 주의

std::mutex, std::atomic 은 복사하거나 이동할 수 없음 -> 멤버 변수에 있을 시 그 클래스는 복사와 이동 x

  • 함수 호출횟수를 세는 경우 등 에서 std::mutex의 대안 = std::atomic 카운터

    • 하지만 남용해서는 안됨 (동기화 필요한 변수가 둘이상이면 부적합한 atomic)
  • 동시적 문맥에서 쓰이지 않을 것이 확실한 경우가 아니라면, const 멤버 함수는 스레드에 안전하게 작성해야함
  • std::atomic 변수는 뮤텍스에 비해 성능상의 이점이 있지만, 하나의 변수 or 메모리 장소를 다룰 때에만 적합

항목17: 특수 멤버 함수들의 자동 작성 조건을 숙지하라

  • 특수 멤버 함수 (C++이 스스로 작성하는 멤버함수)

    • 기본 생성자, 소멸자, 복사 생성자, 복사 대입 연산자
    • 기본 생성자는 클래스에 생성자가 하나도 선언 x
    • public + inline
    • 가상 소멸자가 있는 기반 클래스를 상속하는 파생 클래스의 소멸자를 제외하고는 비가상
  • C++11 의 이동 연산 (작성되는 경우 클래스의 비정적 자료멤버들에 대해 멤버별 이동을 수행)

    1. 이동 생성자 (move constructor)
    2. 이동 대입 연산자(move assignment operator)
1
2
Widget(Widget&& rhs); // move cons
Widget& operator=(Widget&& rhs); // move assignment
  • 멤버별 이동

    • 이동연산이 지원되지 않은 타입들은 복사연산들을 수행
  • 복사 이동 차이

    • 복사 생성, 복사 대입 연산자는 서로 독립적 o
    • 이동 생성, 이동 대입 연산자는 서로 독립적 x
      • 둘 중 하나 선언하면 다른 하나는 컴파일러가 작성 x
  • 복사 연산을 하나라도 명시적으로 선언한 클래스는 이동 연산들이 작성되지 않음 (반대도 마찬가지)

    • 복사 연산 명시함 == 기본 복사 방식이 적합하지 않음 == 이동 연산 또한 부적합 가능성 큼
  • 기존 클래스에 사용자 선언 이동 연산을 추가하는 방법

    • 특수 멤버 함수의 작성에 관한 C++11 규칙을 반드시 따르는 것

** Rule of Three**

  • 3의 법칙

    • 복사 생성자, 복사 대입 연산자, 소멸자 중 하나라도 선언했다면 나머지 둘도 선언해라
  • 직접 지정했다는 의미 == 그 클래스가 자원 관리를 수행하기 때문

    • 이러한 클래스들은 거의 항상
      • 한 복사 연산이 수행하는 자원 관리를 다른 복사 연산에서도 수행해야함
      • 클래스의 소멸자 역시 그 자원관리에 참여 (자원 해제, 자원: 메모리)

이동 연산이 자동으로 생성되는 경우

  • 복사연산 x
  • 이동연산 x
  • 소멸자 x

default로 명시적으로 표현

  • 3rule을 지키기 위해

C++11 규칙들

  • 기본 생성자
    • 생성자 x 경우만
  • 소멸자
    • 기본 noexcept로 생성됨
    • 기본적으로 작성되는 소멸자는 오직 기반 클래스 소멸자가 가상일때
  • 복사 생성자
    • 비정적 데이터들을 멤버별로 복사 생성
    • 사용자 선언 이 없을 경우만 자동 생성
    • 이동연산 -> 자동생성 비활성화
  • 복사 대입 연산자
    • 사용자 선언 이 없을 경우만 자동 생성
  • 이동 연산들

    • 비정적 데이터의 멤버별 이동을 수행
    • 클래스에 복사 연산, 이동 연산, 소멸자가 없을 때 자동 생성
  • 멤버 함수 템플릿이 존재하면 비활성화 된다는 규칙은 없음
    • T 가 자신의 클래스일 경우에만 비활성화 (항목26)

기억 해 둘 사항

  • 이동은 소멸자가 있으면 자동 x
  • 복사가 있으면 이동이 자동 x, 이동이 있으면 복사가 자동 x
  • 복사 생성, 복사 대입, 소멸 은 같이 작성
  • 멤버 함수 템플릿 -> 자동작성 금지 아님 (일부 예외있음)
This post is licensed under CC BY 4.0 by the author.

[ostep] 프로세스 API

[ostep] 제한적 직접 실행 원리