아래 링크의 번역문입니다.
다음은 내가 현재 Columbia University에서 C++ 코스에 등록한 Bjarne Stroustrup 교수의 “A Tour of C++”를 읽을 때 흥미롭거나 생소했던 몇 가지 최신 C++ 기능들이다.
pt.1 - Chapter 1 ~7
(Ch.1 pp.10)
constexpr
은 “컴파일 시간에 평가된다(evaluated)”를 뜻하며 성능을 향상시킬 수 있다.constexpr
함수(side-effect가 없어야 하며 비지역 변수를 수정하지 않아야 한다.) 아에 상수가 아닌 파라미터를 전달 할 때, 그 결과는 상수 표현식이 아닐 수 있다.1 2 3 4
constexpr double half(double x) { return x / 2 }; int num = 4.0; constexpr auto numHalf = half(num); // 잘못됨! num이 상수가 아니므로 // 결과가 constexpr이 아닐 수 있음!
(Ch.1 pp.15) C++17에서 이니셜라이저가 있는 if문. “if문”에서 변수를 초기화하고 조건을 사용할 수 있다. 이것은 변수의 범위(scope)를 더 좁게 유지하는 데 도움된다.
1
if (auto a = arr[0]; a != 's') { /* a의 scope는 여기까지. */ }
변수를 0에 대해 테스트하는 경우 조건을 생략할 수도 있다.
1
if (auto sz = vec.size()) { /* sz != 0인 경우 여기에 진입 */ }
(Ch.1 pp.17)“참조(reference)에 대한 할당은 포인터와 달리 레퍼런스가 참조하는 것을 변경하지 않지만 참조된 개체에 할당한다.”
1 2 3 4
int x = 2, y = 3; int &r1 = x; int &r2 = y; r1 = r2; // x는 3이 된다. r1은 여전히 x를 가리킨다.
- (Ch.1 p.18) 초기화되지 않은 참조는 허용되지 않는다.
- (Ch.2 p.25) 주어진 시간에 여러 후보 변수 중 하나만 사용할 수 있는 경우 union을 사용할 수 있다.
- (Ch.2 p.26) 즉, C++ 17 이후 STL의
variant
은union
의 대부분의 사용 사례를 대체할 수 있다. Type-safe한union
이다. std::get<type>
과 함께try-catch
또는std::holds_alternative
를 사용하여 원하는 값을 얻을 수 있다.
- (Ch.2 p.26) 즉, C++ 17 이후 STL의
- (Ch.2)
enum
(열거자) v.s.enum class
. 후자가 선호된다.일반 열거자 값은 암시적으로 변환된다(예를 들면 int로 변환). 또한 열거자 이름은 ‘주변 scope로 내보낼(export) 수 있음’으로 인해 이름 충돌 가능성이 있다.
1 2
enum Color1 { red, green, blue }; enum Color2 { red, yellow, black }; // 에러! 열거자 'red'를 재정의 함.
enum class
에서 사용된 이름은 다른 클래스에서 선언하여 사용할 수 있다.1 2
enum class Color1 { red, green, blue }; enum class Color2 { red, yellow, black };
- (Ch.3 p.30) 분할 컴파일. user.cpp, Vector.h, 그리고 Vector.cpp 예에서 Vector.h는 user.cpp가 사용할 클래스에 대한 선언이 있는 “인터페이스(interface)”이다. 세부 구현 정보는 Vector.cpp에 명세된다(specified). 두 .cpp 파일 모두 .h 파일을 포함하고, .cpp파일을 별도로 컴파일 할 수 있다.
- (Ch.3 p.31) include된 헤더 파일의 선언 및 매크로는 나중에 include된 헤더 파일에 영향일 미칠 수 있다. 버그의 상당한 원인!
- (Ch.3 p.34) 헤더(구식
#include
)와 모듈(export
/import
) 차이점- 모듈은 한 번만 컴파일된다. 헤더는 사용되는 ‘번역 단위(translation unit)’마다 컴파일 된다.
- (항목 8 참고) include 순서를 변경하면 의미가 달라질 수 있다.
- import는 전이되지 않는다. (즉, X는 Y를 가져오고 Z는 X를 가져온다. Z는 자동으로 Y를 가져오지 않는다.)
- (Ch.3 p.35) “네임스페이스는 주로 라이브러리와 같은 더 큰 프로그램 구성 요소를 구성하는 데 사용된다.”
- (Ch.3 p.37)
noexcept
로 선언된 함수는 예외를 “절대로” throw해서는 안 된다. 그렇지 않으면std::terminate()
가 호출되어 프로그램을 종료한다. - (Ch.4, p.52) 날 것의
new
와delete
를 피하라. 대신 잘 동작하는 추상화된 구현체 내부에 묻어라. 즉, 생성자와 소멸자 내부에서 사용하라. (Ch.4 p.54) 순수 가상 함수(pure virtual function)은 0을 할당하여 선언한다. 파생 클래스는 해당 함수를 “반드시” 정의해야 한다. 순수 가상 함수가 있는 클래스를 “추상 클래스”라고 한다. 추상 클래스는 object를 만들 수 없다. 따라서 추상 클래스에는 일반적으로 “생성자”도 없다.
1 2 3 4 5 6 7 8 9 10
class MyClass { public: virtual int size() = 0; }; class MyClass2 : public MyClass { public: MyClass2() { /* ... */ } int size() override { /*...*/ } // 또는 virtual int size()로 정의할 수 있다. };
- (Ch.4 p.61) 상속에서 “object는 생성자에 의해 ‘기본 클래스’를 우선으로 생성되고, 소멸자에 의해 ‘파생 클래스’를 우선으로 소멸된다.”
(Ch.4 p.62)
dynamic_cast
를 사용해is instance of
작업을 할 수 있다.1 2 3 4 5
template<typename Base, typename T> inline bool IsInstanceOf(T *ptr) { // dynamic_cast는 실패시 0(nullptr)을 반환 return dynamic_cast<Base*>(ptr) != nullptr; }
- (Ch.4 p.64) “
new
를 사용하여 생성된 객체를 삭제하는 것을 잊지 않으려면unique_ptr
또는shared_ptr
을 사용하라.” (Ch.5 p.68) “단일 인수를 쓰는 합당한 이유가 없는 한 생성자에 대해
explicit
을 사용하라.”1 2 3 4 5 6 7 8
class Vector { public: explicit Vector(int sz); // int에서 Vector로의 암시적 변환 방지 /* ... */ }; Vector vec1(7); // OK Vector vec2 = 7; // 에러
- (Ch.5 p.72)
std::move()
는 캐스트와 비슷하다. 실제로 아무것도 옮기지 않는다. - (Ch.5 p.75) Range-based for는
.begin()
및.end()
를 암시적으로 사용한다. (Ch.6 p.83) C++17부터
std::pair
의 템플릿 인수(type)를 추론할 수 있다.1 2
std::pair<int, double> p1 = { 1, 3.41 }; // C++17 이전 std::pair p2 = { 1, 3.41 }; // C++17 이후
pt.2 - Chapter 8 ~ 14
(Ch.8 p.110) C 표준 라이브러리의 각 헤더 파일에는 “이름 앞에 c가 붙고 .h가 없는” 버전이 있다. 이 버전은
std::
네임스페이스를 사용한다.1 2 3 4 5
// C 스타일 #include <stdlib.h> // C++ 스타일 (std::) #include <cstdlib>
- (Ch.9 p.112)
std::string::replace()
의 새 대체 문자열은 대체되는 대상 substring과 길이가 같을 필요가 없다. (Ch.9 p.113) 문자열 리터럴은 기본적으로
const char*
이다.std::string
타입을 원하면 s 접미사가 필요하다.1 2 3 4 5 6 7 8
/* * "s" 접미사는 아래 namespace 중 하나를 include 해야 한다. * - using namespace std::literals * - using namespace std::string_literals * - using namespace std::literals::string_literal */ auto s1 = "Ian"; // const char* (C 스타일) auto s2 = "Pan"s; // std::string
- (Ch.9 p.113) “짧은 문자열 최적화”: 짧은 문자열 값은 문자열 object에 유지되고(특수 최적화), 일반적인 경우에는 더 긴 문자열만 free-store(heap에 동적으로 할당된 메모리)에 배치된다.
- (Ch.9 115)
string_view
는 해당 문자의 읽기 전용 보기이다. - (Ch.9 p.116)
R"(
로 시작하고)"
로 끝나는 “원시 문자열 리터럴”은 백슬래시를 문자열에서 직접 사용할 수 있도록 한다. 예를 들어"\\w{2}\\d{4}"
는R"(\w{2}\d{4})"
로 쓸 수 있다. - (Ch.10 p.125) 출력 stream에서 “출력 표현식의 결과는 뒤따르는 출력에 사용될 수 있다”. 이게 여러 “put-to”(
<<
)문을 연결할 수 있는 이유다. - (Ch.10 p.126) 입력 stream에 대해서도 마찬가지다. 여러 “get-from”(
>>
)문을 연결할 수 있다.- 문자가 숫자(digit)가 아닌 경우 정수 읽기가 중지된다. 또한
>>
는 기본적으로 초기 공백을 건너뛴다.
- 문자가 숫자(digit)가 아닌 경우 정수 읽기가 중지된다. 또한
- (Ch.10 p.128)
istream1 >> char1
을 조건으로 사용할 수 있다. “istream1
에서char1
으로 문자를 성공적으로 읽었나?”를 의미한다. - (Ch.10 p.128)
istream >> ch
는 기본적으로 공백을 건너뛰지만istream.get(ch)
는 그렇지 않다. (Ch.10 p.129) 조정자(manipulator)를 사용한 형식화(출력 stream과 출력할 대상 사이에 위치시킨다.)의 예.
1
std::cout << std::hex << 1234; // "4d2"
다른 조정자로
std::oct
,std::scientific
,std::hexfloat
등이 있다.(Ch. 10 p.130)
std::cout.precision()
또는std::setprecision()
은 부동 소수에 대한 길이 제한으로 반올림할 수 있다. 소수점 이하가 아니라 “전체” 길이를 제한한다.1 2
std::cout.precision(7); std::cout << 1234.567890; // 1234.568 (전체 길이는 7)
그러나 원래 숫자가 정수인 경우 “정밀도(
precision
)”는 영향을 미치지 않는다.1 2
std::cout.precision(4); std::cout << 123456789; // 123456789 (자르기/반올림 되지 않음)
기본 정밀도를 복원하려면 수정하기 전에 저장해야 한다.
1 2 3 4 5
auto x = std::cout.precision(); // 정밀도 수정하기 std::cout.precision(4); // 복원하기 std::cout.precision(x);
(Ch.10 p.132) C 스타일의 I/O(
printf
,scanf
)는 더 나은 I/O 성능을 제공합니다. C 스타일 I/O를 사용하지 않고 I/O 성능에 신경쓴다면 다음을 호출할 수 있다.1
std::ios_base::sync_with_stdio(false);
- (Ch.10 p.132) 파일 시스템 라이브러리
<filesystem>
을 사용하면 파일 시스템 경로를 표현하고, 파일 시스템을 탐색하여 파일 유형 및 관련 권한을 검사할 수 있다. (Ch.11 p.141)
std::vector
의 첨자 연산자는 범위를 확인하지 않는다. 오류가 발생하지 않는 대신 임의(random) 값을 제공한다! 범위를 확인하는 것은 추가 비용을 의미하기 때문이다.1 2
std::vector<int> v{ 1, 2, 3, 4 }; std::cout << v[100]; // 0. 에러 throw 없음
.at()
을 사용하면 범위를 확인하고out-of-range
오류를 발생시킨다.
1 2 3
std::vector<int> v{ 1, 2, 3, 4 }; std::cout << v.at(100); // 'std::out_of_range' 인스턴스를 throw한 후 종료됨
- (Ch.11 p.142)
std::list
는 doubly-linked다. 삽입 및 삭제가 빈번한 경우std::vector
가 좋다.- (Ch.11 p.143) STL에는
std::forward_list
라는 singly-linked list가 있다. 각 노드에는 포인터가 1개만 있기 때문에 공간을 절약할 수 있다.
- (Ch.11 p.143) STL에는
- (Ch.11 p.143) 모든 STL 컨테이너는
begin()
및end()
함수를 제공한다. - (Ch.11 p.143)
std::equal_range()
는std::lower_bound()
및std::upper_bound()
값을 포함하는std::pair
을 반환한다. - (Ch.11 p.144)
std::map
(associative array, dictionary 등으로 부름)은 균형 이진 검색 트리이며, 일반적으로 red-black 트리로 구현된다.- 존재하지 않는 키에 기본값을 삽입하지 않으려면
[]
대신find()
를 사용하라. std::map
의 조회(lookup) 비용은O(log n)
다.
- 존재하지 않는 키에 기본값을 삽입하지 않으려면
(Ch.11 p.147)
emplace_back()
과 같은 Emplace 작업은 object를 컨테이너에 복사하는 대신, 요소(element)의 생성자에 대한 인수를 사용해 컨테이너의 새로 할당된 공간에 object를 빌드한다.1 2 3
vector<pair<int, string>> v; v.push_back(make_pair(1, "hello")); v.emplace_back(2, "hi"); // pair<int, string> object를 build함
- (Ch.11 p.148) exclusive-or 연산자(
^
)를 표준 해시 함수를 결합하여 요소에 대한 좋은 해시 함수를 만들 수 있다. - (Ch.12 p.150)
std::back_inserter()
는 컨테이너 뒤에 값을 추가하는 데 사용할 수 있는 iterator(std::back_insert_iterator
)를 반환한다. 이 함수를 사용하면 C 스타일이고 오류가 발생하기 쉬운realloc()
사용을 피할 수 있다. - (Ch.12 p.153) iterator는 훌륭한 중개인이 될 수 있다. 이를 이용해 STL 알고리즘은 컨테이너에 대해 아무것도 모른 채 컨테이너의 데이터에 대해 연산(operate)할 수 있다.
- (Ch.12 p.157) STL 알고리즘은 컨테이너의 요소를 추가하거나 빼지 않는다. 하고 싶다면, 컨테이너 메서드(
push_back()
,erase()
) 또는back_inserter()
와 같이 컨테이너에 대해 알고 있는 함수가 필요하다. 찾기, 계산, 바꾸기, 정렬, 복사와 같은 많은 STL 알고리즘에는 추가 인수를 전달하여 호출할 수 있는 병렬 버전이 있다.
1 2 3
sort(begin(vec), end(vec)); // 기본, 순차 sort(std::execution::seq, begin(vec), end(vec)); // 기본, 순차 sort(std::execution::par, begin(vec), end(vec)); // 병렬
- (Ch.13 p.165) object의 여러
shared_ptr
인스턴스가 소유권을 “공유”한다. object는 “마지막 공유 포인터”가 소멸될 때 소멸된다. 고유 포인터(unique_ptr
)는 공유 포인터가 수행하는 “use count”를 추적할 필요가 없기 때문에 일반적으로 더 효율적이다. - (Ch.13 p.168) STL 컨테이너의 “moved-from” 상태는 보통 “비어있음”을 말한다.
- (Ch.13 p.171) C 스타일 배열에 비해
std::array
는 시간/공간 오버헤드가 없다. (Ch.13 p.171)
std::array
의 두 번째 템플릿 인수(크기)는 상수 표현식이 들어가야 한다.1 2
int n = 3; std::array<char, n> arr{'a', 'b', 'c'}; // 에러: 크기가 상수 표현식이 아님
다음과 같이 n을
const
또는constexpr
로 지정해야 한다.1 2 3 4 5
const int n = 3; std::array<char, n> arr{'a', 'b', 'c'}; // good constexpr int n = 3; std::array<char, n> arr{'a', 'b', 'c'}; // good
함수 매개변수는 상수 표현식이 아니기 때문에 함수 매개변수를 배열의 크기로 사용할 수 없다.
1 2 3 4 5 6 7 8 9
void foo(const int n) { std::array<char, n> arr{'a', 'b', 'c'}; // 에러! // ... } void bar(constexpr int n) { // 잘못된 constexpr! std::array<char, n> arr{'a', 'b', 'c'}; // ... }
이 문제를 해결하려면 다음과 같이 템플릿으로 만들어야 한다.
1 2 3 4 5 6 7 8 9 10
template <int n> auto func2() -> std::array<int, n> { std::array<int, n> a{1, 2, 3}; return a; } int main() { auto a = func2<3>(); // ... }
(Ch.13 p.171)
std::array
는.data()
를 사용하거나 첫 번째 요소의 명시적 주소를 사용하여 포인터가 필요한 C 스타일 함수에 전달할 수 있다.1 2 3 4 5 6 7 8 9
void func1(int *p, int sz); void func2() { const int n = 3; std::array<int, n> a{1, 2, 3}; func1(a, n); // 에러! func1(&a[0], n); // good func1(a.data(), n); // good }
- (Ch.13 p.172) 내장 C 스타일 배열에 비해 std::array의 장점은 다음과 같다.
- STL 알고리즘으로 사용이 간편하다.
=
연산자로 복사할 수 있다.- 포인터로 형 변환되지 않는다.
(Ch.13 p.174) C++17에 “생성자 인수에서 템플릿 인수 타입 추론”이 추가되었다.
1 2 3 4
tuple<string, int> tup1{"Ian Pan", 123}; // C++17 이전 tuple tup2{"Ian Pan"s, 123}; // C++17 이후 // 대안... auto tup3 = make_tuple("Ian Pan"s, 123);
튜플에서 요소를 가져오는 것은 다음과 같이 수행할 수 있다.
1 2
auto s1 = get<0>(tup1); auto s2 = get<string>(tup1);
또한
get<>()
을 사용하여 튜플에 기록한다.1 2
get<0>(tup1) = "Columbia"; get<string>(tup1) = "University";
- (Ch.13 p.176)
optional<T>
는variant<T, nothing>
과 같은 특별한 종류의 변형으로 볼 수 있다. - (Ch.13 p.184)
std::enable_if
는 조건부로 정의하는 데 널리 사용된다(예: 연산자 멤버 함수 정의). (Ch.14 p.192) 난수를 생성하기 위해 “engine”와 “distribution”(예: uniform, Gaussian, exponential 등)을 지정하고 “generator”를 만들 수 있다.
1 2 3 4 5 6 7
using my_engine = default_random_engine; using my_distribution = uniform_int_distribution<>; my_engine eng{}; my_distribution dice{1, 6}; auto rollDice = [&]() { return dice(eng); }; auto num = rollDice();
- (Ch.14 p.192)
std::valarray
는 요소별 수학 연산은 물론 슬라이싱 및 shifting(rolling)을 지원하는 벡터와 유사한 컨테이너이다. - (Ch.14 p.193) 조언: “시퀀스에서 값을 계산하는 루프를 작성하기 전에
accumulate()
,inner_product()
,partial_sum()
, 그리고adjacent_difference()
를 고려하라.”