/ The Blog of Jinho Ko / Computer science / Programming / C++ / Effective C++

·

6 min read

3. Modern C++ 에 적응하기

By Jinho Ko

Item 7. Distinguish between () and {} when creating objects.

기존 다양한 초기화 방법

Widget w1;
Widget w2 = w1; // call copy constructor
w1 = w2;

C++11에서는 하나의 초기화 구문을 위해 uniform initialization을 도입했으며, 중괄호{} 초기화라고 한다. 중괄호 초기화의 기능 중 하나는, narrowing conversion을 방지해준다는 것이다. 즉 내부 expr이 값으로 표현 안될 경우 에러 발생시킴.

단점은, std::initializer_list와 연관된 overloading에서 발생한다. 중괄호 초기화는 std::initializer_list 형태의 파라미터를 강하게 선호한다 (심지어 겉으로 보기에 더 잘 맞는 params가 있어도). 즉 해당 생성자가 호출되는 것이다. 대표적 예가 std::vector을 쓸 때인데, 중괄호 사용 시 initializer_list가 선택되기 때문에 아래 두 호출은 다른 결과를 낳는다.

vector<int>(10, 20) // elem 10, length 20
vector<int>{10, 20} // len 2 vector with 10, 20

이는 명백한 디자인 오류이다. 따라서 무엇을 쓸 때인지를 명확히 인지하고 일관되게 사용해야 한다.

Item 8. Prefer nullptr to 0 and NULL

NULL은 포인터 형식이 아니다. nullptr의 장점은 정수 형식이 아니란 것인데, 엄밀하게는 std::nullptr_t이고, 모든 종류의 pointer type으로 implicit casting됨. 절대 정수 type으로 해석되지 않기 때문에 예상하지 않은 overloading이 발생할 일은 없음.

또, 아래와 같이 API 호출 이후 auto로 결과를 받아올 때, 0과 nullptr 사이 ambiguity 또한 해소가 가능하다.

auto result = API()
if (result == nullptr) {

}

Item 9: Prefer alias declarations to typedefs.

C++11은 alias declaration을 지원한다.

using X = .........

Alias declaration이 좋은 이유는, typedef에 반해 템플릿화가 가능하다는 것이다.

template<typename T>
using X = std::..<T>

typedef를 사용할 때 MyAllocList<T>::type와 같이 ::type를 붙여야 하는데, 우선 귀찮고, 컴파일러는 이것이 형식 이름임을 항상 확신하지 못한다. (i.e. type이라는 이름을 가지는 멤버가 존재한다면?)

Item 10: Prefer scoped enums to unscoped enums.

scoped enum은 enum class {}로 정의하는 enum을 의미.

그냥 enum을 쓰게 되면 내부 이름들이 외부에 노출된다.

enum Color {black, red, }
auto red = false; // ERROR!

또 scoped enum은 다른 형식으로 implicit casting이 되지 않기 때문에 static_cast<>를 사용하지 않는 이상 실수를 범할 일이 없다.

또 scoped enum은 항상 forward declaration이 가능하다 (enum이 업데이트될 때 발생하는 recompile을 최소화함). (scoped enum의 default underlying_type은 int이다.)

Item 11: Prefer deleted functions to private undefined ones.

구현체를 제공할 때 특정 함수가 호출되지 않게 하기 위해 CPP98에서는 해당 함수를 private 처리했다. (주로 copy constructor) private으로 선언하고, 정의하지 않으면 되는 방식이었다.

C++11에서는 = delete를 사용한다. 삭제된 함수는 어떠한 방법이든 사용할 수 없고, 또 public으로 선언하는 것이 관례이다. 우선 접근여부를 확인한 후에 삭제여부를 확인해야 하기 때문이다.

=delete는 private과 다르게 어떠한 함수에든 적용 가능하다. 즉 클래스 밖의 함수에도, 적용 가능하다.(원치 않는 overloading을 막을 때 사용 가능) 또 원치 않는 template instantiation에도 사용가능하다.

template<>
void process<void>(void*) = delete

위 예제는 void*의 template type 생성을 방지한 것이다. (만약 엄밀하게 생성 제한을 하고자 한다면, const, const volatile, … 과 같은 자매들도 다 차단해야 하긴 함.)

Item 12: Declare overriding functions override.

C++의 OOP 핵심 중 하나는, derived class의 virtual function 구현이 base class의 가상함수 구현을 재정의한다는 것이다. 재정의 시 함수의 signature 포함, 모든 요구조건이 맞아야 실제 재정의가 일어나는데, 이는 작은 실수가 큰 차이를 만들어내기 때문이다. 그리고 그 실수가 발생하더라도 컴파일러는 이를 잡아내지 못한다. 이를 위해 재정의 의도를 명확하게 나타내어 override를 달아줘야 함.

Item 13: Prefer const_iterators to iterators.

C++98에서는 const_iterator를 사용하는 것이 매우 까다로웠으나, C++11에서는 완화되었다.

Item 14: Declare functions noexcept if they won’t emit exceptions.

함수의 noexcept여부는 함수의 const만큼 중요한 정보이다. 가장 큰 장점은 opt를 극강으로 가능하게 한다는 것이다. C++98에서 exception이 호출되면 stack unwinding 및 termination이 이뤄지는데, noexcept가 붙게 되면 1. 스택을 unwindable하게 유지할 필요가 없고, 2. obj destruction 순서도 마음대로 가져갈 수 있다.

대신 당연히 catch block은 수행이 안될 것이다. 하지만 noexcept 상황 자체에서 exception이 발생하는 것 자체가 프로그램 종류 사유로써 충분하다.

C++의 push_back같은 연산은 C++11에서 주어진 연산의 noexcept의 선언 여부에 따라 move semantics를 따를지 copy를 할지 결정됨. 또 다른 예로 stl알고리즘의 swap()이 있는데, swap()은 조건부 noexcept로써, 내부 expr이 noexcept인지 아닌지에 따라 그 noexcept 여부가 결정된다.

noexcept의 문제는 처음에 noexcept로 구현을 했다가 나중에 마음이 바뀌는 경우인데, 이런 경우 만족할만한 수습 방안이 없다. 또 noexcept를 억지로 만족하기 위해 status code를 return하는 로직을 추가한다면 추가 로직이 붙을 수 있으므로 썩 좋은 design은 아니다.

모든 deallocation 함수는 암묵적으로 noexcept이다. C++에서는 noexcept가 보장되지 않은 noexcept선언도 허용한다. 즉 noexcept의 선언과 그 책임은 결국 개발자한테 있는 것이다.

Item 15: Use constexpr whenever possible.

constexpr은 compile time에 const는 처음 선언 이후 그 값이 변경할 수 없는 값을 의미한다. 즉 constconstexpr를 포함한다.

constexpr은 정수 상수 표현식(integral constant expression)이 요구되는 데에서 쓸 수 있다. 예를 들어,

constexpr auto sz = 10;
std::array<int, sz> data; // OK!

함수를 constexpr로 정의하면, argument가 모두 compile time에 결정되는 경우 그 계산이 컴파일 도중에 이뤄지고 결과가 프로세스의 data영역에 저장될 수 있다. 만약 아닌 경우라면 일반적인 함수랑 똑같이 작동한다.

constexpr은 객체/함수의 인터페이스의 일부이며, 이를 지정한다는 것은 이 함수(객체)를 C++가 상수 표현식을 요구하는 문맥에서 사용가능하다는 의미이다.

Item 16: Make const member functions thread safe.

const 함수가 mutable 변수를 변경하려 들 때는 thread-safe하지 않다. 즉 mutex나, std::atomic과 같은 연산을 이용해서 thread safe하게 사용해야 한다.

Item 17: Understand special member function generation.

C++에는 사용자가 정의하지 않아도 컴파일러가 자동으로 작성하는 멤버함수들이 있다.

default constructor, destructor, copy constructor (A(A&), copy assignment constructor (operator=(&)), move constructor (A(A&&)), move assignment constructor (operator=(&&))이 그것이다. 이 함수들은 암묵적으로 public, inline, nonvirtual하다.

어떤 함수들이 자동으로 생성되는지는 실용적인 관점에서의 엄청 논린적인 이유에 의해 복잡한 rule에 따라 결정된다. 여기에 의존하는 것을 명시할 때 =default를 붙인다. 이는 derived class의 객체를 조작하는 interface 정의할 때 유용하게 사용한다.

규칙 정리

  • default constructor: 사용자 선언 생성자 없을 시 생성
  • destructor: 사용자 선언 없을 시. 기본적으로 noexcept임. base class destructor가 virtual일 때만 virtual임
  • copy const: 클래스에 사용자 정의 copy const가 없을 때만 자동으로 작성. move const가 작성되어 있으면 삭제(=delete)됨.
  • copy asssign: 클래스에 사용자 정의 copy assign가 없을 때만 자동으로 작성. move const가 작성되어 있으면 삭제(=delete)됨.
  • move const / move assign: 사용자 선언 copy/move/dest가 모두 없을때만 자동 생성된다.
last modified June 2, 2024
comments powered by Disqus

© Copyright 2024. Jinho Ko. All rights reserved. Last updated: June 02, 2024.