항목 7: 객체 생성 시 ()와 {}를 구분하자.
객체 생성 방법
1. int x(0);
2. int x = 0;
3. int x{0};
4. int x = {0}; //3과 사실상 동일하기에 3으로 통일하여 설명함.
중괄호 초기화가 소괄호 초기화보다 좋은점
1. 모든 초기화 상황에 적용가능함.
//비정적 자료 멤버 기본 초기화
Class Widget
{
int x(0); // 에러
int y = 0; // 가능
int z{0}; // 가능
}
//복사할 수 없는 객체 초기화 (항목 40)
std::atomic<int> x(0); // 가능
std::atomic<int> y = 0; // 에러
std::atomic<int> z{0}; // 가능
2. 좁히기 변환 방지
double x,y,z;
...
int sum1{x+y+z} // double의 합을 int가 커버 못헤서 에러남
3. 기본 생성자 호출 시 실수안함
Widget w1{}; // 기본 생성자
Widget w2(); // 함수 선언으로 인식되어버림
클래스 중괄호 초기화 주의사항
클래스를 중괄호로 초기화하면 다음과 같이 동작함.
1. 호환되는 std::initializer_list 매개변수 생성자가 있는지 찾고 있으면 그걸로 생성
2. 없으면 일반 매개변수 생성자를 실행
여기서 문제는 1에서 호환되는지를 검사할 때 굉장히 굉장히 쉽게 호환 시킨다.
class Widget
{
public:
Widget(int i, bool b); // 1
Widget(int i, double d); // 2
Widget(const Widget& w); // 3
Widget(Widget&& w); // 4
Widget(std::initializer_list<long double> il); // 5
operator float() const;
}
Widget w0;
Widget w1(10, true); // 1
Widget w2(10, 5.0); // 2
Widget w3(w0); // 3
Widget w4(std::Move(w0)) // 4
Widget w5{10,true}; // 5
Widget w6{10, 5.0}; // 5
Widget w7(w0); // 5
Widget w8(std::Move(w0)) // 5
//그냥 어떻게든 변환가능한 타입이면 최우선으로 선택
따라서 중괄호 생성자는 조심해서 만들고 왠만해선 다른 생성자랑 안 헷갈리도록 만들자.
근데 std::vector<int>에서 (1,2)랑 {1,2}도 다른 생성자 호출되긴함. 우린 그러지말자.
참고로 기본생성자가 있다면 Widget w{}는 기본 생성자가 우선으로 선택됨. 빈 std::initalizer로 가진 않음.
Widget w({}) 이건 빈걸로 감.
결론
다음 두 방식중 하나로 통일해서 사용하자.
1. 생성을 ()을 베이스로 하고 반드시 {}를 써야할 땐 {}를 쓰기
2. 생성을 {}를 베이스로 하고 반드시 ()를 써야할 땐 ()를 쓰기
결론은 둘 다 장단점이 있을거라 하지만 앞 설명에서 볼때 글쓴이는 2를 선호하는 것 같다. modern c++이 주제인 책이라 그런 걸 수도.
항목 8: NULL, 0 대신 nullptr쓰자.
근거
1. 포인터랑 정수에 대한 overload가 있으면 0, NULL은 정수로 빠져버림, nullptr은 포인터로 간다.
void f(int); // 1
void f(void*); // 2
f(0); // 1
f(NULL); // 1
f(nullptr); // 2
2. auto랑 같이 쓸때 정수인지 포인터인지 알려주기 좋음
auto x = f();
if(x == nullptr) // x == 0으로하면 x가 정수인지 포인터인지 헷갈림.
{
...
}
3. 템플릿에 넘길때 NULL, 0은 정수로 넘어가지만 nullptr은 포인터로 넘어감.
리터럴 0은 포인터로 자동 형변환이 된다. 하지만 int는 안된다.
int* p;
p = 0; // 널 포인터로 대입
int x = 0;
p = x; // 에러
템플릿 매개 변수에 nullptr를 넘기면 포인터 타입으로 넘어가 내부에서 포인터로 변환 가능하지만 0을 넘기면 int 0으로 넘어가 포인터로 변환 시 컴파일 에러남
template<typename T>
void f(T p)
{
void* p1 = p;
}
f(nullptr) // 쌉가능
f(0) // 에러
항목 9: typedef 대신 Using쓰자.
C++11엔 다음과 같은 별칭 선언을 제공한다.
Using NewType = const int*;
//이제 NewType은 const int*임
이게 typedef보다 좋다.
근거
1. 함수 포인터 선언할때 훨씬 보기 직관적임.
typedef void (*FP)(int, const std::string&);
using FP = void (*)(int,const std:string&);
2. 템플릿화 가능함.
template<typename T>
using MyAllocList = std::list<T,MyAlloc<T>>;
MyAllocList<Widget> lw; // std::list<Widget,MyAlloc<Widget>> lw;
//typedef도 구조체안에 해서 가능하지만 템플릿 내부에서 쓸때 typename을 붙여야하는등 불편함.
형식 특질
C++11에 타입의 const, refrence등을 제거할 수 있는 형식 특질 기능을 다음과 같이 제공함.
std::remove_const<T>::type
std::remove_reference<T>::type
std::add_lvalue_reference<T>::type
하지만 이건 템플릿 구조체 typedef를 이용한 거라 템플릿안에서 쓸땐 typename을 붙여야 하는등 불편함.
그래서 C++14에 Using을 써서 만든 다음 버전을 제공함.
std::remove_const_t<T>
std::remove_reference_t<T>
std::remove_lvalue_refernece_t<T>
만약 C++11에서 위 버전을 사용하고 싶다면 직접 만들자.
template<class T>
using remove_const_t = typename remove_const<T>::type;
항목 10: enum 대신 enum class를 쓰자.
enum과 enum class 차이
1. enum은 {} 바깥에서 원소 값들에 바로 접근되지만 enum class는 네임스페이스 명시해주어야함.
enum Color = {black, white};
Color a = white; // 가능
auto white = false; //에러, 이름 겹침.
//---------------------
enum class Color = {black, white}
Color a = white // 에러
Color a = Color::white // 가능
auto white = false; // 가능
2. enum은 정수 자동 형변환 가능, enum class는 자동으로 안됨. static_cast등으로 바꿔주어야함.
3. enum은 바탕 형식을 직접 지정해주지 않으면 정의로 부터 바탕 형식을 계산하기에 선언이 불가능함. enum class는 int로 고정이고 직접 지정 시 해당 형식으로 됨. 따라서 직접 지정안해도 선언 가능
enum Color = {black, white}; // 선언 불가능, 쓰는 곳마다 정의 넣어줘야함.
enum Color: std::uint32_t; // 선언 가능
enum Class Color; // 선언 가능
enum Class Color :: std::uint32_t; // 선언 가능
결론
보통의 경우 네임스페이스의 깔끔함, 정수로 자동 변환이 안되어 실수 방지 가능 등에 의해 enum class가 좋다. 하지만 enum값을 정수로 자동 형변환하여 사용하고 싶을땐 enum을 쓰자.
항목 11: delete
기존에 C++98에선 복사 생성자등을 가리기 위해 private선언 후 정의하지 않기를 사용했다. C++11부턴 = delete를 쓰자.
더 좋은점
1. 사용하면 링킹이 아닌 컴파일 시점에 오류 띄어줌
2. 클래스 멤버 함수 템플릿의 특정 특수화도 delete는 지울 수 있음.
3. 비멤버 함수의 일부 overload를 지워 일부 형식의 자동형변환 호출을 막을 수 있음.
멤버함수 delete 관례
멤버함수 지울땐 public에 두고 지우자. private에 두면 호출 시 delete가 아닌 private로 에러가 뜰 수 있고 비 직관적임.
항목 12: 재정의 시 override 키워드를 넣자.
안 넣으면 매개변수 다름, 함수 이름 잘못씀, const 성 다름, 참조 한정사 다름등에 의해 오버라이드 실패시에도 에러가 안떠 큰일 날 수 있다. override적으면 안될 시 에러떠서 바로 알 수 있음.
참조 한정사
class Widget
{
pulbic:
void f1() &; //LValue Widget으로 f1 호출 시 호출
void f1() &&; //RValue Widget으로 f2 호출 시 호출
}
참조 한정사가 달라도 오버라이드 안되니 주의하자.
항목 13: 가능하다면 const 반복자를 쓰자.
이는 C++98에서도 마찬가지였지만 98에서는 const 반복자를 쓰기 너무 불편해서 잘 안썼다. 모던 C++에서는 잘 제공해주니 사용하자.
STL 컨테이너에 cbegin, cend등으로 const 반복자를 바로 얻을 수 있으며 find등의 함수에서 const 반복자를 입력하면 const 반복자를 바로 리턴해준다. 그러한 const 반복자에 insert 함수들을 바로 사용해서 쓰면 된다.
템플릿에서 포인터의 경우까지 일반화하려면 cbeing 멤버 함수가 아닌 cbeing 전역 함수로 객체로부터 반복자를 얻어야한다. 이는 C++14에선 제공하지만 C++11에선 없기에 직접 만들어야한다.
templat <class C>
auto cbegin(const C& container) -> decltype(std::begin(container))
{
return std::begin(Container)
}
항목 14: 예외를 방출하지 않는 함수는 noexcept로 선언하라
noexcept를 함수에 선언한다고 진짜 예외가 자동으로 막히거나 하지는 않는다. 예외는 그대로 발생한다. 대신 컴파일러는 해당 함수에서 예외가 발생하지 않을 것이라 가정하고 최적화를 할 수 있게된다. 대신 예외가 발생하면 정상적으로 콜스택을 타고 예외가 throw되지않고 그냥 크래쉬가 날 수 있다.
어쨌든 다음 이익을 위해 함수가 예외를 던지지 않는다면 noexcept를 선언하자.
1. 사용하는 클라이언트가 안심하고 쓸 수 있더.
2. 컴파일러가 최적화 할 수 있다.
3. 코드에서 noexcept인 경우에 대한 처리를 했을때 더 효율적인 처리로 이어질 수 있다.
코드를 작성할 때 특정 함수의 noexcept여부를 true,false로 받을 수 있다.
noexcept(함수호출)을 쓰면된다.
vector의 경우 위 기능을 이용하여 배열의 메모리를 확장할 때 이동을 쓸지 복사를 쓸지를 원소 이동 연산의 noexcept 여부를 확인하여 선택한다. 즉 noexept의 여부가 중요한 성능 문제로 직결된다.
추가로 특정 템플릿 함수의 noexept여부를 조건문으로 설정할 수 있다.
template <class c>
void f(c c1) noexcept(조건문);
주로 특정 다른 함수의 noexcept여부에서 파생한다.
항목 15: 가능하면 constexpr를 사용하라
constexpr는 함수나 변수 앞에 붙일 수 있다. constexpr는 컴파일 시점에 사용가능한 것이라는 의미이고 변수의 경우 const도 가지게 된다.
변수는 컴파일 타임에 계산가능한 리터럴 또는 constexpr로 이루어진 표현식으로 초기화해야하며 함수도 그러한 것들로 매개변수를 받고 그러한 것을 리턴해야한다.
이러한 변수나 함수는 배열의 크기나 템플릿 상수등에 넣어 사용할 수 있다.
또한 클래스이 생성자도 constexpr로 할 수 있고 그러한 생성자로 생성한 클래스는 constexpr 변수로 사용할 수 있으며 constexpr 멤버 함수들을 사용할 수 있다.
constexpr 멤버 함수라고 const함수는 아니기에 멤버를 수정할 수도 있지만 constexpr 클래스 인스턴스에는 사용하지 못하고 주로 내부 멤버 함수안에서 인스턴스를 만들고 수정한뒤 constexpr 인스턴스로서 리턴할때 사용된다.
항목 16: const 멤버 함수는 스레드에 안전하게 작성하자.
const 함수는 기본적으로 멤버를 읽기만 하기에 스레드에 안전하다고 고려된다. 하지만 내부적으로 mutable멤버를 수정한다면 스레드에서 안전하지 않을 수 있다. 이를 방지하기 위해 mutex나 atomic 변수를 사용해야한다.
atomic 변수는 각종 연산이 스레드들에 대해 원자적으로 작동하는 것이 보장되나 성능이 조금 더 드는 변수이다.
주로 한 변수에 대해서만 관리해야할 땐 atomic을, 여러 변수를 관리해야할땐 mutex를 쓴다.
항목 17: 기본 생성 함수들 생성 조건
클래스의 기본 생성 함수들의 생성 조건은 아래 각 조건이 만족된 상태에서 사용되면 생성된다.
기본 생성자: 생성자 선언이 하나도 없을때
소멸자: 무조건 생성, 부모가 virutal 소멸자면 자동 생성도 virtual로 생성
복사 생성자: 복사 생성자, 이동 연산가 하나도 선언되어 있지않으면 생성
복사 대입: 복사 대입, 이동 연산가 하나도 선언되어 있지 않으면 생성
이동 성생자: 복사 연산, 이동 연산이 하나도 선언되어 있지 않으면 생성
이동 대입: 이동 생성자와 동일
이동 연산들의 생성 조건은 5의 법칙에 따라 복사 연산, 이동 연산, 소멸자 중 하나라도 있다면 자원 관리를 하는 객체일 확률이 높고 그에 따라 나머지 모든 것이 존재해야할 확률이 높기에 저렇게 정해졌다.
복사 연산들이 소멸자나 복사 연산에 대해 저렇게 작동하지 않는 것은 기존 C++98에서는 그것을 생각하지 못했기에 C++98의 코드와 호환성을 위해 복사, 소멸자에 대해선 막지 못했고 이동에 대해서만 막았다. 하지만 복사, 소멸자가 있는 상태에서 특정 복사 연산의 기본을 자동으로 만들어 쓰는건 비권장 행동으로 정의되어있다.
추가로 위 함수들의 선언뒤에 =default를 붙히면 명시적으로 기본 생성을 해준다.
자동으로 생성될때에도 명시적으로 default를 선언해주는게 좋다.
이동 연산이 없을때 이동 연산은 복사 연산으로 대체되는데 기본 생성된 이동 연산을 default 명시 없이 사용하다 소멸자가 추가되어 기본 연산이 사라졌을때 이를 눈치 채지 못하면 복사 연산으로 대체 되어 성능이 떨어진다.
참고 서적
Effective Modern C++
'프로그래밍 언어 > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] 항목 31 ~ 34 : 람다 표현식 (0) | 2024.12.15 |
---|---|
[Effective Modern C++] 항목 23 ~ 30 : 이동, 완벽 전달 (1) | 2024.11.23 |
[Effective Modern C++] 항목 18~22 : 스마트 포인터 (0) | 2024.11.10 |
[Effective Modern C++] 항목 5~6 : auto (0) | 2024.11.03 |
[Effective Modern C++] 항목 1~4 : 형식 영역 (0) | 2024.11.03 |