항목7: 객체 생성시 괄호(())와 중괄호({})를 구분하라
C++11 초기값 지정
- 괄호로 지정
- 등호로 지정
- 중괄호로 지정
대입이 항상 일어나지는 않음
Widget w1
: 기본 생성자Widget w2 = w1
: 대입 x, 복사 생성자w1 = w2
; : 대입 O, 복사 대입 연산자
균일 초기화(uniform initialization)
- 중괄호 초기화: 어디에서나 사용할 수 있음
- non-static 멤버의 기본 초기화값 지정 가능
- 복사할 수 없는 객체 (std::atomic) 초기화 가능 (“=”로는 불가)
균일 초기화 장점
다양한 문맥에서 사용 가능
암묵적 좁히기 변환(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
- 가장 성가신 구문 해석(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에 의존함
- MyAllocList
- 타입말고 다른 것을 지칭할 가능성이 있기 때문
- 의존적 타입의 이름앞에는 typename 을 붙여야하는 C++의 규칙
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>
- struct:
항목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
과 같은 비멤버함수가 없음
- 주의점: C++11에서는
- 특정 멤버 함수 대신, 그 멤버 함수에 상응하는 비멤버 함수 를 사용 (
항목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문)
- 요령: 삼항연산자, 재귀
- 실행 가능 문장이 많아야 하나 (대부분 return문)
C++14 제약
- C++11 보다 느슨, for문 등 가능
반드시 리터럴 타입(컴파일 시 결정되는 타입)을 받고, 리턴해야함
생성자
- 생성자 또한 constexpr 가능
- 객체를 읽기 전용 메모리 안에 생성 가능
mid.xValue() * 10
과 같은 표현식을 템플릿 인수나 열거자의 값을 지정하는 표현식에서 사용 가능C++11에서는 setter 함수들을 constexpr로 선언하지 못함 (두가지 이유)
- 작동 대상 객체를 수정
- C++11에서는 constexpr 멤버함수는 암묵적으로 const로 선언됨
- 반환 타입이 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 의 이동 연산 (작성되는 경우 클래스의 비정적 자료멤버들에 대해 멤버별 이동을 수행)
- 이동 생성자 (move constructor)
- 이동 대입 연산자(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
- 복사 생성, 복사 대입, 소멸 은 같이 작성
- 멤버 함수 템플릿 -> 자동작성 금지 아님 (일부 예외있음)