Item 7: Distinguish between () and {} when casting objects
C++11에서 객체 초기화는 혼란스럽다.
일반적인 규칙은, 초기화 값을 parentheses(소괄호), equal sign, 또는 braces(중괄호)로 지정하는 것이다.
equal sign과 braces를 함께 사용하는 것도 가능하다.
C++는 일반적으로 euqals-sign-plus-braces 문법을 braces-only와 같은 방식으로 처리한다.
int x(0); // initializer is in parentheses
int y = 0; // initializer follows "="
int z{0}; // initializer is in braces
int w = {0}; // initializer use "=" and braces
Uniform initialization, Braced initialization
Uniform initialization은 하나의 초기화 문법이 어디에서나 어떤것이든 표현할 수 있다는 것을 의미한다.
이것은 braces를 기반으로 하기 때문에, braced initialization으로도 부른다.
Braced initialization을 사용하면 이전에는 표현할 수 없던 것들을 표현할 수 있다.
중괄호를 사용하면, container의 초기값을 지정하는 것이 쉽다.
또한 중괄호는 non-static data member의 default initialization values를 지정하는데 사용될 수 있다.
C++ 11에서 이것은 "=" 초기화 문법에서도 가능하지만, 소괄호로는 안된다.
Uncopyable objects 또한 중괄호 또는 소괄호를 사용하여 초기화될 수 있지만, "="는 사용할 수 없다.
braced initialization은 모든 초기화 상황에서 사용될 수 있으며, 이것이 "uniform"이라고 불리는 이유다.
std::vector<int>v{1,2,3}; // v's initial content is 1,2,3
class Widget{
private:
int x{0};
int y = 0;
int z(0); // error!
};
std::atomic<int> ai1{0};
std::atomic<int> ai2(0);
std::atomic<int> ai3 = 0; // error!
1) Prevents implicit narrowing conversions
Braced initialization은 built-in types간의 암시적인 narrowing conversion을 금지한다.
double x, y, z;
int sum1{ x + y + z }; // error!
int sum2( x + y + z );
2) Immune to C++'s most vexing parse
문법의 모호함으로 겪는 문제로, 개발자는 객체를 default로 생성하고자 하지만, 결국 함수 선언으로 처리하는 문제다.
만약 클래스의 생성자에 어떤 인자도 전달하지 않고 생성자를 호출한다면, 객체가 아닌 함수를 선언하게 된다.
함수는 중괄호를 사용하여 파라미터 리스트를 선언할 수 없기 때문에, 객체의 default constructor를 중괄호를 사용하여 호출하면 이러한 문제가 발생하지 않는다.
Widget w2(); // most vexing parse! declares a function
Widget w3{}; // calls Widget ctor with no args
생성자 오버로딩 해소 과정에서 중괄호 초기화는 가능한 한 std::initializer_list 매개 변수가 있는 생성자와 부합한다.
생성자 호출에서 std::initializer_list 매개변수가 관여하지 않는 한 소괄호와 중괄호의 의미는 같다.
Things to Remember
Braced initialization은 가장 널리 사용되는 초기화 문법이며 narrowing conversion을 방지하고, C++의 most vexing parse에 면역이다.
Item 8: Prefer nullptr to 0 and NULL
1) Avoid overload resolution surprises
C++98 프로그래머를 위한 가이드라인에서는 포인터와 정수형 타입에 대한 오버로딩을 피하라고 한다.
이것은 0과 NULL 때문인데, 0과 NULL은 null pointer를 표현하기 위해 사용되지만 실제로는 int와 0L(long) 타입이다.
따라서 0과 NULL을 인자로 전달하여 함수를 호출하면 f(int)를 실행하게 된다.
nullptr의 장점은 정수형 타입이 아니라는 것이다.
정확히 pointer 타입은 아니지만, 모든 타입의 포인터라고 생각할 수 있다.
순환적인 정의에서, nullptr의 실제 타입은 std::nullptr_t이다.
std::nullptr_t는 nullptr의 타입으로 정의되고, std::nullptr_t는 암시적으로 모든 raw pointer types으로 변환된다.
따라서 오버로드된 함수를 nullptr을 인자로 전달하여 호출하면 포인터에 대한 함수가 호출된다.
void f(int);
void f(void*);
f(0); // calls f(int), not f(void*)
f(NULL); // might not compile, but typically calls f(int)
f(nullptr); // calls f(void*)
2) Improve code clarity
특히 auto를 사용할 때, nullptr을 사용하면 코드 명확성이 향상된다.
auto result = findRecord( /* arguments */ );
if(result == 0){}
위 코드에서 result가 포인터인지 정수형 타입인지 명확하지 않다.
그러나 0을 nullptr로 바꾼다면 result가 포인터 타입이라는 것을 명확히 할 수 있다.
auto result = findRecord( /* arguments */ );
if(result == nullptr){}
Things to Remember
0과 NULL보다는 nullptr를 사용해라.
integral, pointer types에 대한 오버로딩을 피해라.
Item 9: Prefer alias declrations to typedefs
Alias declarations(타입 별칭)
using을 사용하여 타입을 선언하는 방식으로 typedef를 대체할 수 있다.
// 1. typedef
typedef std::nique_ptr<std::ordered_map<std::string, std::string>> UPtrMapSS;
// 2. Alias declarations
using UPtrMapSS = std::nique_ptr<std::ordered_map<std::string, std::string>> UPtrMapSS;
// do exactly same thing
Alias templates
alias declarations은 typedef와 다르게 템플릿화를 지원한다.
C++11에서는 C++98에서 템플릿 구조체 내부에 중첩된 typedef를 함께 사용했던 복잡한 방식과 달리 간단한 메커니즘을 제공한다.
// 1. typedef
template<typename T>
struct MyAllocList{
typedef std::list<T, MyAlloc<T>> type;
};
MyAllocList<Widget>::type lw; // client code
// 2. alias template
template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>;
MyAllocList<Widget> lw; // client code
typedef의 경우 접미사로 "::type"을 명시해야 한다.
게다가, 클래스 템플릿에서 사용하는 경우, typedef로 선언한 타입을 사용하기 위해서 typename을 명시해야 한다.
MyAllocList<T>::type이 template class내에 위치하는 경우, T에 의존하기 때문에 dependent type이 된다.
C++에서 dependent type의 이름은 typename 앞에 명시해야 한다.
그러나 alias templates을 사용하면 이러한 추가적인 명시 없이도 간단하게 타입을 사용할 수 있다.
MyAllocList가 alias template이기 때문에 컴파일러는 MyAllocList<T>가 type의 이름이라는 것을 안다.
따라서 MyAllocList<T>는 non-dependent type이고, typename specifier가 허용되지 않는다.
// 1. typedef
template<typename T>
class Widget{
private:
typename MyAllocList<T>::type list;
};
// 2. alias template
template<tupename T>
class Widget{
private:
MyAllocList<T> list;
};
Type traits
C+11은 type traits의 형태로 타입 특성 변환을 수행하는 도구를 지원한다.
header <type_traits>안에 다양한 템플릿이 있다.
std::remove_const<T>::type // yields T from const T
std::remove_reference<T>::type // yields T from T& and T&&
std::add_lvalue_reference<T>::type // yields T& from T
C++11의 type traits는 nested typedefs inside templatized structs처럼 구현된다.
따라서 이러한 변환의 각 끝에는 "::type"을 명시해야 한다.
그리고 template내에 type parameter로 사용하고자 하는 경우, 각각의 앞에 typename을 명시해야 한다.
C+14에서는 모든 C+11 타입 특성 변환에 대해 alias templates를 제공한다.
std::remove_const<T>::type // C+11: const T -> T
std::remove_const_t<T> // C+14 equivalent
Things to Remember
typedef는 템플릿화를 지원하지 않지만, alias declarations은 템플릿화를 지원한다.
Alias templates는 "::type" 접미사와 템플릿에서 typedef를 참조하는데 종종 필요한 "typename"접두사를 피한다.
C++14는 모든 C++11 타입 특성 변환에 대해 alias templates을 제공한다.
Item 10: Prefer scoped enums to unscoped enums
C++98-style enum: Unscoped enum
unscoped enum과 같은 enumerator는 한 중괄호 쌍 안에서 어떤 이름을 선언하면 그 이름의 visibility(가시성)는 중괄호 쌍이 정의하는 범위로 한정된다는 일반적인 규칙이 적용되지 않는다.
enum을 포함하는 범위에 속한 enumerator들의 이름은, 해당 범위의 다른 어떤 것도 동일한 이름을 가질 수 없다.
enum Color{ black, white, red }; // black, white, and red are in same scope as Color
auto white = false; // error! white already declared in this scope
C++11: Scoped enums
Scoped enums은 "enum class"를 통해 선언된다.
Enumerators가 오직 enum내에서만 보이기 때문에 namespace pollution을 피한다.
enum class Color { black, white, red }; // black, white, red are scoped to Color
auto white = false; // fine
Color c = white; // error! no enumerator named white is in this scope
Color c = Color::white; // fine
Scoped enums은 unscoped enums과 달리 암시적 형변환을 허용하지 않는다.
따라서 다른 타입으로 형변환을 해야 하는 경우 cast를 사용해야 한다.
enum class color { black, white, red };
Color c = Color::red;
static_cast<double>(c);
Foward-declared(전방선언)
Scoped enums은 enumerators의 지정 없이 forward-declared될 수 있다.
C+11에서 Unscoped enum도 전방선언이 가능한데, 이는 모든 enum의 underlying type이 정수 타입이라는 사실에서 비롯된다.
Underlying type이란 enumerators가 프로그램 안에서 실제로 저장되는 정수 형식을 말한다.
enum Status{
good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
indeterminate = 0xFFFFFFFF
};
위 코드에서 값은 0부터 0xFFFFFFFF범위 안에 존재한다.
컴파일러는 Status의 값을 표현하기 위해 char보다 큰 integral type을 선택할 것이다.
메모리를 효율적으로 사용하기 위해서, 컴파일러는 종종 작은 underlying type을 선택하기를 원한다.
C++98은 오직 enum definition에서만 이를 지원한다. enum declaration은 허용되지 않는다.
이것은 컴파일러가 enum이 사용되기 전에 각 enum의 underlying type을 선택할 수 있도록 한다.
forward-declared enums은 compilation dependencies(컴파일 의존성)를 증가시킨다는 단점이 있다.
만약 새로운 값이 Status enum에 추가되면, 모든 시스템이 다시 컴파일되어야 한다.
C++11의 enum을 사용하면 이러한 단점을 보완할 수 있다.
enum class Status; // forward delcaration
void continueProcessing(Status s); // use of fwd-declared enum
만약 Status가 수정되고, continueProcessing의 행동에 영향을 주지 않는다면, continueProcessing의 구현은 다시 컴파일될 필요가 없다.
enum을 사용하기 전에 컴파일러는 enum의 사이즈를 알아야 한다.
Scoped enums의 underlying type은 항상 알려지기 때문에 전방선언이 가능하다.
Unscoped enums에서 전방선언을 사용하기 위해서는 underlying type을 지정해야 한다.
기본적으로, scoped enums의 underlying type은 int이다.
enum class Status; //underlying type is int
enum class Status: std::unint32_t; //underlying type for Status is std::uint32_t
enum Color: std::uint32_t; //fwd decl for unscoped enum
Things to Remember
C++98-style enums은 unscoped enums이다.
Scoped enums의 enumerators는 enum내에서만 보이며 오직 cast를 통해서만 다른 타입으로 변환될 수 있다.
Scoped, unscoped enum둘다 underlying type의 지정을 지원한다.
Scoped enum의 default underlying type은 int이며, unscoped enums은 default underlying type이 없다.
Scoped eunms은 항상 forward-declared될 수 있다.
Unscoped enums도 forward-declared될 수 있지만, underlying type이 선언에서 지정되어야 한다.
'C++ > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] Chapter 4: Smart Pointers (0) | 2022.07.26 |
---|---|
[Effective Modern C++] Chapter 2: Auto (0) | 2022.07.19 |
[Effective Modern C++] Chapter 1: Deducing Types (1) | 2022.07.18 |