본문 바로가기

업무용 언어/C++

C++11 살펴보기


C++11

C++11

  1. 설계 목표
  2. C++ 핵심 언어 확장
    1. 핵심 언어 런타임 성능 향상
      1. 우측값 참조 및 이동 생성자
      2. constexpr – 일반화된 상수 표현식
      3. Plain Old Data(POD) 정의 변경
    2. 핵심 언어 빌드 타임 성능 향상
      1. extern 템플릿
    3. 핵심 언어 사용성 개선
      1. 이니셜라이저 목록 std::initializer_list<T>
      2. 단일 형태 초기화 {}
      3. 타입 추론 auto, decltype
      4. 범위 기반 for 반복문
      5. 람다 함수 및 표현식 []() -> T {}
      6. 선택적 함수 문법 (반환 타입 지정)auto () -> T {}
      7. 생성자 위임
      8. 명시적 overridefinal
      9. nullptr
      10. 강력한 형식의 열거형 enum class
      11. 오른쪽 꺽쇠 괄호 >>
      12. 명시적 변환 연산자
      13. 템플릿 별칭
      14. 무제한 공용 구조체

    4. 핵심 언어 기능성 개선
      1. Variadic 템플릿
      2. 새로운 문자열 리터럴
      3. 사용자 정의 리터럴
      4. 멀티스레딩 메모리 모델
      5. 스레드 로컬 저장소
      6. 특수 멤버 함수에 붙는 defaultdelete
      7. long long int 타입
      8. static_assert
      9. 확장된 sizeof
      10. 객체 정렬 조절 및 확인
      11. 쓰레기 수집기 구현 허가
      12. 특성 (Attributes)


  3. C++ 표준 라이브러리 변경
    1. 표준 라이브러리 컴포넌트 업그레이드
    2. 스레드 프로그래밍 편의
    3. 튜플 타입
    4. 해시 테이블
    5. 정규 표현식
    6. 다목적 스마트 포인터
    7. 확장 가능한 랜덤 수 기능
    8. 래퍼 참조
    9. 함수 객체를 위한 다형성 래퍼
    10. 메타프로그래밍을 위한 타입 trait
    11. 함수 객체 반환 타입 계산을 위한 단일 형식 메소드

constexpr

상수 표현식의 사용은 컴파일러에게 최적화의 기회를 준다. 컴파일러가 3+4와 같은 상수 표현식을 만나면 미리 계산해 놓고, 프로그램 내에 하드 코딩해 놓는다.

C++ 명세에서는 상수 표현식이어야만 하는 곳들이 몇 군데 있는데, 바로 배열의 정의와 열거형 선언이 그러하다.

int get_five() {return 5;}

int some_value[get_five() + 7]; // Create an array of 12 integers. Ill-formed C++

위에서 get_five 는 항상 5를 반환하여 의미론적으로 상수와 다를 바 없지만, 컴파일러는 이를 알 길이 없기 때문에 컴파일을 거부한다.

C++11에서는 다음과 같은 문법이 가능하다.

constexpr int get_five() {return 5;}

int some_value[get_five() + 7]; // Create an array of 12 integers. Legal C++11

컴파일러는 get_five가 컴파일 타임 상수임을 이해하고, 실제로 컴파일 타임 상수인지를 확인하게 된다.

constexpr을 함수에 사용하게 되면 몇가지 제약 사항이 생긴다.
첫째로, void가 아닌 반환 타입을 가져야 한다. 둘째로, 함수 본체에서 변수나 새로운 타입을 정의할 수 없다. 셋째로, 함수 본체는 선언, null 문장만이 존재해야 하며, 딱 하나의 return 문만 가져야 한다.

C++11 이전에 변수의 값은 const로 선언되고, 상수 표현식으로 된 이니셜라이저를 가지며, 정수형 혹은 열거형 타입이어야만 상수 표현식으로 사용될 수 있었다. C++11에서는 constexpr가 붙으면, 정수형 혹은 열거형 타입이어야만 한다는 제약을 없앴다.

constexpr double earth_gravitational_acceleration = 9.8;
constexpr double moon_gravitational_acceleration = earth_gravitational_acceleration / 6.0;

extern 템플릿

C++03에서는 하나의 번역 단위에서 완전하게 지정된 템플릿에 도달할 때마다 템플릿을 반드시 인스턴스화 해야 했다. 만약 똑같은 템플릿이 여러 번역 단위에서 인스턴스화 된다면 컴파일 타임이 상당히 길어질 수 있다. C++03에서 이를 막을 수 있는 방법이 없었기에, C++11에서는 새로이 extern 템플릿 선언을 도입하였는데, extern 데이터 선언과 비슷하다.
C++03에서는 아래의 문법으로 컴파일러에게 템플릿 인스턴스화를 강제할 수 있었는데:

template class std::vector<MyClass>;

C++11는 다음과 같은 문법을 제공한다:

extern template class std::vector<MyClass>;

이는 컴파일러에게 이 번역단위에서 템플릿을 인스턴스화 하지 말 것을 알려준다.

이니셜라이저 목록

C++03는 C로부터 이니셜라이저 목록을 계승받았다. 구조체나 배열을 멤버가 정의된 순서대로 중괄호로 쌓여진 목록으로 초기화 한다. 이니셜라이저 목록은 재귀적으로도 사용할 수 있어서, 다음과 같이도 가능하다.

struct Object {
    float first;
    int second;
};

Object scalar = {0.43f, 10}; //One Object, with first=0.43f and second=10
Object anArray[] = {{13.4f, 3}, {43.28f, 29}, {5.934f, 17}}; //An array of three Objects

이 방법은 정적 리스트나 구조체를 어떤 값으로 초기화할 때 아주 유용하다. C++는 생성자를 통해 객체를 초기화할 수도 있는데, 이는 이니셜라이저 목록만큼 편리하지는 않다. 하지만, C++03는 Plain Old Data (POD)에 대해서만 이니셜라이저 목록을 사용할 수 있다; C++11는 std::vector와 같은 표준 컨테이너 뿐만 아니라 모든 클래스에서도 이니셜라이저 목록을 사용할 수 있게 해준다.
std::initializer_list를 인자로 갖는 생성자나 함수를 만들어 두면, 이니셜라이저 목록을 인자로 받을 수 있다:

class SequenceClass {
public:
    SequenceClass(std::initializer_list<int> list);
};

위와 같이 만들면, 다음과 같이 사용할 수 있다:

SequenceClass some_var = {1, 4, 5, 6};

이러한 생성자를 이니셜라이저 목록 생성자라고 부르며, 이런 생성자를 갖는 클래스는 단일 형태 초기화를 사용할 수 있게 된다. (하단 참조)
std::initializer_list<> 클래스는 일급 C++11 표준 라이브러리 타입이며, C++11 컴파일러에서 {} 문법을 통해서만 정적으로 생성할 수 있다. 이 목록은 한번 생성되면 복사는 가능하지만, copy-by-reference 방식이다. 이니셜라이저 목록은 상수이므로 일단 한번 생성되면 값을 바꿀 수 없다.

이니셜라이저 목록이 타입이기 때문에, 클래스 생성자 뿐만 아니라 일반 함수에서도 쓰일 수 있다:

void function_name(std::initializer_list<float> list);

function_name({1.0f, -3.45f, -0.4f});

또한 표준 컨테이너는 다음과 같은 방법으로 초기화될 수도 있다:

std::vector<std::string> v = { "xyzzy", "plugh", "abracadabra" };
std::vector<std::string> v({ "xyzzy", "plugh", "abracadabra" });
std::vector<std::string> v{ "xyzzy", "plugh", "abracadabra" }; // see "Uniform initialization" below

단일 형태 초기화

C++03는 초기화에 있어 몇가지 문제점을 가지고 있다. 초기화를 위한 여러가지 방법이 존재하며, 때때로 교환했을 때 다른 결과를 초래하기도 한다. 전통적인 초기화 문법은 함수 선언같이 보이므로, 컴파일러는 이 문법을 파싱하기 위해 추가적인 단계가 필요하다(파싱이 어려워진다는 뜻).
C++11 어느 객체에서나 동작하는 완전한 단일 형태의 초기화 문법을 제공한다. 이니셜라이저 목록 문법을 확장한 것이다:

struct BasicStruct {
    int x;
    double y;
};

struct AltStruct {
    AltStruct(int x, double y) : x_{x}, y_{y} {}

    private:
        int x_;
        double y_;
};

BasicStruct var1{5, 3.2};
AltStruct var2{2, 4.3};

위의 코드는 모두가 예상한대로 동작할 것이다.

또한 다음과 같이 쓸 수도 있다:

struct IdString {
    std::string name;
    int identifier;
};

IdString get_string() {
    return {"foo", 42}; // 굳이 타입을 적어주지 않아도 됨.
}

단일 형태 초기화는 생성자 문법을 대체하지는 않는다. 생성자 문법이 때때로 필요하기 때문이다. 예를 들어보자. 만약 클래스가 이니셜라이저 목록 생성자를 가지고 있다면, (TypeName(initializer_list<SomeType>);), 다른 형태의 생성자보다 우선시된다. C++11의 std::vector는 이니셜라이저 목록 생성자를 가지고 있다. 그러므로 아래 코드는:

std::vector<int> the_vec{4};

이니셜라이저 목록 생성자를 호출할 것이다. 벡터는 원소 4 한 개를 갖게 될 것이다. 벡터의 초기 사이즈를 결정하는 생성자 호출이 아니란 말이다. 벡터의 초기 사이즈를 지정하고 싶다면, 일반적인 생성자 문법으로 벡터를 생성해야 한다.

타입 추론

C++03 (그리고 C)에서 변수를 사용하려면, 타입이 명시적으로 지정되어야 한다. 반면, 템플릿 타입과 템플릿 메타프로그래밍 테크닉의 출현과 함께 무언가의 타입(특히 잘 정의된 함수의 반환 값)은 쉽게 표현되지 않을 수 있다. 따라서, 중간 값을 변수에 담기는 어려우며, 아마 주어진 메타프로그래밍 라이브러리 내부에 대한 지식을 알아야 할지도 모른다.
C++11에서는 두가지 방법으로 이를 완화시킬 수 있다.
첫째로, 명시적 초기화와 함께 정의된 변수에 auto 키워드를 사용할 수 있다:

auto some_strange_callable_type = std::bind(&some_function, _2, _1, some_object);
auto other_variable = 5;

some_strange_callable_type의 타입은 std::bind 템플릿 함수가 반환하는 타입으로 정해진다. 사용자에게는 쉽지 않은 타입 결정을, 컴파일러는 절차적인 의미론 분석을 통해 쉽게 결정할 수 있다.
other_variable는 사용자에게나 컴파일러에게나 쉽게 타입이 결정된다. 이 타입은 int이며 정수 리터럴 5과 같은 타입이다.
둘째로, decltype 키워드로 표현식의 타입을 컴파일 타임에 결정할 수 있도록 한다:

int some_int;
decltype(some_int) other_integer_variable = 5;

auto 변수의 타입은 컴파일러만 알고 있기 때문에 decltype 키워드는 auto 키워드와 사용될 때 더욱 유용하다. 연산자 오버로딩이나 특화된 타입을 많이 사용하는 표현식에서 decltype이 매우 유용하게 사용될 수 있다.
auto는 또한 코드의 장황함을 줄여주는데 도움을 준다. 예를 들어, 다음과 같이 쓰는 대신

for (std::vector<int>::const_iterator itr = myvec.cbegin(); itr != myvec.cend(); ++itr)

다음과 같이 줄여쓸 수 있다.

for (auto itr = myvec.cbegin(); itr != myvec.cend(); ++itr)

“myvec”은 begin/end 반복자가 구현되어 있기 때문에 다음과 같이 좀더 줄여 쓸 수 있다:

for (auto& x : myvec)

컨테이너를 중첩시키고 typedef를 통한 코드의 양 줄이기를 썼을 때 더욱 커진다(? 해석 불가).

decltype에 의해 지정된 타입은 auto에 의해 추론된 타입과 다를 수도 있다.

#include <vector>
int main() {
    const std::vector<int> v(1);
    auto a = v[0];        // a has type int
    decltype(v[1]) b = 1; // b has type const int&, the return type of
                          //   std::vector<int>::operator[](size_type) const
    auto c = 0;           // c has type int
    auto d = c;           // d has type int
    decltype(c) e;        // e has type int, the type of the entity named by c
    decltype((c)) f = c;  // f has type int&, because (c) is an lvalue
    decltype(0) g;        // g has type int, because 0 is an rvalue
}

범위 기반 for 반복문

C++11는 요소의 범위를 쉽게 순회할 수 있는 for 문법을 확장하였다:

int my_array[5] = {1, 2, 3, 4, 5};
// double the value of each element in my_array:
for (int &x : my_array) {
    x *= 2;
}
// similar but also using type inference for array elements
for (auto &x : my_array) {
    x *= 2;
}

이 형식의 for문은 “범위 기반 for”로 불리며(주로 다른 언어에서는 “foreach”라고 불림), 리스트의 각 요소를 순회한다. C-스타일의 배열, 이니셜라이저 목록, 또는 반복자를 반환하는 begin()end() 함수가 정의된 모든 타입에서 동작하며, 또한 begin/end 쌍을 가지고 있는 모든 표준 컨테이너에서도 잘 동작한다.

람다 함수 및 표현식

C++11은 람다라고 불리는 익명 함수를 만들 수 있다. 다음과 같은 형태이다.

[capture](parameters) -> return_type { function_body }

다음은 람다 함수의 예:

[](int x, int y) -> int { return x + y; }

C++11은 클로져도 지원한다. 클로져는 람다 표현식에서 대괄호인 [] 사이에 정의된다. 값에 의한 캡쳐와 참조에 의한 캡쳐를 지원한다.

[]        //no variables defined. Attempting to use any external variables in the lambda is an error.
[x, &y]   //x is captured by value, y is captured by reference
[&]       //any external variable is implicitly captured by reference if used
[=]       //any external variable is implicitly captured by value if used
[&, x]    //x is explicitly captured by value. Other variables will be captured by reference
[=, &z]   //z is explicitly captured by reference. Other variables will be captured by value

값에 의한 캡쳐 변수는 기본적으로 상수이다. 매개 변수 목록 다음에 mutable 키워드를 추가하면 매개 변수 목록이 비상수가 된다.

[capture](parameters) mutable -> return_type { function_body }

다음 두 예시는 람다 표현식의 사용법을 보여준다.

std::vector<int> some_list{ 1, 2, 3, 4, 5 };
int total = 0;
std::for_each(begin(some_list), end(some_list), [&total](int x) {
    total += x;
});

이 예는 벡터에 들어있는 모든 요소의 합계를 구한다. total 변수가 람다 함수의 내부로 캡쳐되어 사용된다. total 변수는 스택 변수에 대한 참조이므로 값을 변경할 수 있다.

std::vector<int> some_list{ 1, 2, 3, 4, 5 };
int total = 0;
int value = 5;
std::for_each(begin(some_list), end(some_list), [&, value, this](int x) {
    total += x * value * this->some_func();
});

total은 참조로, value는 복사로 전달했다.
this의 캡쳐는 특별한 의미를 갖는데, 참조로는 캡쳐할 수 없고 값으로만 캡쳐할 수 있다. this는 람다를 감싸고 있는 가장 가까운 함수가 비정적 멤버 함수일 때에만 캡쳐할 수 있다. 람다는 protected/private 멤버에 대해 람다가 정의된 멤버 함수와 똑같은 접근 권한을 가진다.
If this is captured, either explicitly or implicitly, then the scope of the enclosed class members is also tested. Accessing members of this does not require explicit use of this-> syntax.
The specific internal implementation can vary, but the expectation is that a lambda function that captures everything by reference will store the actual stack pointer of the function it is created in, rather than individual references to stack variables. However, because most lambda functions are small and local in scope, they are likely candidates for inlining, and thus will not need any additional storage for references.
If a closure object containing references to local variables is invoked after the innermost block scope of its creation, the behaviour is undefined.
Lambda functions are function objects of an implementation-dependent type; this type’s name is only available to the compiler. If the user wishes to take a lambda function as a parameter, the type must be a template type, or they must create a std::function or a similar object to capture the lambda value. The use of the auto keyword can help store the lambda function,

auto my_lambda_func = [&](int x) { /*...*/ };
auto my_onheap_lambda_func = new auto([=](int x) { /*...*/ });

다음은 람다 함수를 변수, 배열, 벡터에 저장하고, 함수의 매개 변수로 람다를 전달하는 예시이다.

#include <functional>
#include <vector>
#include <iostream>

double eval(std::function <double(double)> f, double x = 2.0)
{
    return f(x);
}

int main()
{
    std::function<double(double)> f0    = [](double x){return 1;};
    auto                          f1    = [](double x){return x;};
    decltype(f0)                  fa[3] = {f0,f1,[](double x){return x*x;}};
    std::vector<decltype(f0)>     fv    = {f0,f1};
    fv.push_back                  ([](double x){return x*x;});
    for(int i=0;i<fv.size();i++)
        std::cout << fv[i](2.0) << std::endl;
    for(int i=0;i<3;i++)
        std::cout << fa[i](2.0) << std::endl;
    for(auto &f : fv)
        std::cout << f(2.0) << std::endl;
    for(auto &f : fa)
        std::cout << f(2.0) << std::endl;
    std::cout << eval(f0) << std::endl;
    std::cout << eval(f1) << std::endl;
    std::cout << eval([](double x){return x*x;}) << std::endl;
    return 0;
}

빈 캡쳐 리스트로 된 람다 표현식은 같은 타입의 함수 포인터로 암묵적으로 변경할 수 있다. 따라서 다음이 가능하다.

auto a_lambda_func = [](int x) { /*...*/ };
void (* func_ptr)(int) = a_lambda_func;
func_ptr(4); //calls the lambda.