요새 modern cpp를 공부한다고 cpp11과 cpp14를 공부하고 있다.
뭐가 이렇게 새로운게 많은 것인가.. 싶다.
이런것도 모르고 cpp를 쓰고 있었다니 그런데 한 숨에 이해하기 어려운 것들도 있는데, 나름대로 이해해서 적어 보았다.
많은 C++ 개발자들이 C를 배우기도 했고, 최신 C++의 기능들을 적용하지 못하고 있다는게 이런 코드들이 (우리회사에서도) 잘 사용되지 않는 이유인 듯 했다.

C++11 wiki 참고하였다.

Table of Contents:


Lvalue, Rvalue

MS Docs. Rvalue Reference 참고.

  • lvalue: left value이자, locator value.
    Rvalue reference를 통해, 기존에 발생할 수 있는 문제들을 해결하고, move constructor의 기반으로 사용.
    int& lv = 1;    // error Lvalue 참조
    int&& rv = 1;   //       Rvalue 참조
    

move constructor 모두의 코드 참고

  • 즉 복사 생성자는 하나하나 싹 다 복사하는 거고,
  • 이동 생성자는 포인터 주소만 복사하는 것.
  • 그래서 이동 생성자에서 각 멤버들이 source의 멤버가 가리키는 주소를 가리키게 하고, 그 주소가 초기화(delete)되지 않도록 함.
  • 말 그대로 복사는 싹 복사해서 하나 더 만듦.
  • 말 그대로 이동은 있는건 쓱 옮김.

&&의 형식 연역

&&이 그렇다고 항상 Rvalue가 되는 것은 아니다. &&는 경우에 따라 universal reference로도 사용된다.

void f(Widget&& param);   //Rvalue reference
Widget&& var = Widget();  //Rvalue reference
auto&& var2 = var;        //universal reference

template<typename T>
void f(std::vector<T>&& param); //Rvalue reference
template<typename T>
void f(T&& param);              //universal reference

&&가 쓰이면서 Rvalue가 되는 경우를 보면 형식연역이 일어나지 않는 경우이다.
반면 아래 Type Inference에서 더 확인하겠지만 형식 연역이 일어나는 경우 즉, auto나 template에서는 입력받은 자료형에 따라 형식 연역이 일어나기 때문에 받은 param 값이 중요하다.
universal reference는 초기값이 왼값이면 왼값 참조, 초기값이 오른값이면 오른값 참조를 하게된다.

extern template

extern template class std::vector<MyClass>과 같이 template에 extern을 사용할 수 있게 되었음.

Uniform Initialization

C++11에서 uniform initialization을 도입하여, 초기화 구문의 혼동을 완화하고, 어디서나 사용할 수 있는 초기화 구문을 도입함.
다른 초기화 방식((), =)에서 제대로 초기화되지 않는 부분들도 잘 초기화될 수 있음.
특히 서로 다른 값을 담는 STL 컨테이너를 직접 생성 후 값들을 추가했어야 하는 부분들이 쉽게 초기화 할 수 있게 되었음.

class widget {
    private:
        int x{0};   // ok
        int y = 0;  // ok
        int z(0);   // error
}

std::atomic<int>a1{0};      // ok
std::atomic<int>a2(0);      // ok
std::atomic<int>a3 = 0;     // error

double x, y;
int sum{x + y};     // error. double can't be int. sum(x+ y) can do it.

// C++98
Person p1{20, "Tom"};
Person p2{19, "Jane"};

// C++11
vector<Person> vec{ {21, "Smith"}, {23, "Mary"} };

Member Initializer List

생성자가 길어지는 것을 줄일 수 있고, 효율적으로 초기화시킬 수 있음.

// C++98
Point(int ax, int ay) { x = ax, y = ay;}

// C++11
Point(int ax, int ay) : x(ax), y(ay) {/* more if need */}
  • cpp는 int n(3);과 같은 초기화가 가능하기 때문.
  • x(ax)대신 x{ax}도 가능.
  • 기존 대입식 초기화는 객체를 위한 메모리 생성/할당 후 여기에 값을 넣고 이 값을 멤버변수에 대입.
  • 반면 초기화 리스트는 객체의 생성과 초기화를 한번에 하기 때문에 오버헤드를 줄일 수 있음.
  • 반드시 초기화 리스트가 필요한 경우 (== 대입식 초기화로 안되는 경우)
    1. 클래스가 레퍼런스를 멤버변수로 갖는 경우
    2. 클래스가 non static + const 멤버 변수를 갖는 경우
    3. 자식 클래스가 부모 클래스의 private 변수를 초기화하는 경우
    4. 자식 클래스가 부모 클래스를 초기화하는 경우
    • ex, child(int ai, int aj, int ak): parent(ai, aj), k(ak)

Type Inference

C++11에서 autodecltype키워드가 추가되었다.

auto 키워드.

  1. auto 타입을 통해 파이썬처럼 자료형을 명시하지 않고 사용할 수 있게 되었음.
  2. auto 타입은 항상 초기화를 필요로 하기 때문에, 코드에따라 초기화가 되지 않을 수 있어 발생하던 문제들을 보완할 수 있는 역할을 함.
  3. 이식(다른 bit의 프로그램)성 문제에서 형식 불일치가 발생하는 경우가 없어짐.
  4. 예를 들면 std::function객체를 사용하면 auto를 사용하는 것보다 일반적으로 메모리를 많이 사용하고 속도도 느림.
    • auto는 클로저를 담는 변수와 클로저와 같은 형식. std::functionstd::function 템플릿의 한 인스턴스 크기이고, 고정되어 있음. 클로저를 저장하기에 부족한 경우, 힙 메모리를 할당해서 클로저를 저장함.
  5. 코드의 길이도 더 짧아짐.

auto vs auto&& stackoverflow 참고.

  • auto는 항상 local copy.
  • auto&는 항상 reference.
  • auto&&는 local인지 reference인지 생각하지 않음. 즉 형식연역에 따라 둘다 될 수 있음.

decltype 키워드.
decltype(declared type)은 컴파일러가 type을 결정하도록 하는 것. type을 알려주는 것.
auto 키워드가 method의 return type으로 쓰일 때, return type을 알지 못하면 error가 발생함.

  • 그러면 후행 반환 형식으로 type을 명시하거나, decltype을 사용해야 함.
    auto를 사용하여 함수의 리턴 타입을 추론할 때 const나 참조형이 있다면 속성이 없어져 버리는데 decltype(auto)를 사용하면 정확히 추론된다.

Range-based for loop

java에서처럼 :을 통한 for문을 사용할 수 있다. for (int i: arr)
array에서 배열의 길이를 지정해주지 않아도 되며, i의 초기값을 정해주지 않아도 된다.
또한, 언제까지 순회할지를 지정하지 않아도 된다.

  • 이건 내가 원하는데까지 순회할 수 없다는 단점도 됨.

응용:

  • 변경 방지: for (auto const i:arr)

std::array

vector에서 지원하는 함수들을 유사하게 지원하면서, array를 만들어서 구현할 수 있또록 함. array[]와의 차이:

  1. std::array는 객체이기 때문에 크기를 알 수 있음. arr.size()
  2. std::array는 index가 비어있어도 됨.
  3. std::array는 생성시 크기를 명시해야 함.
  4. std::array는 대입이 가능함.

Cpp Reference 참고.

  • 단순히 C의 array[]를 가지고 있는 집합체로, C의 array와 동일한 성능을 내면서 size를 얻거나, 할당 지원과 같은 장점을 얻을 수 있음.

lambda functions

lambda 식이 추가 되었는데, Java의 lambda 식과는 약간 다르고(뭔가 더 구시대적인 느낌?) 함수를 간단하게 만드는 것이라고 볼 수 있음.
auto calc = [](int a, int b) -> int {return a + b;}와 같이 사용할 수 있음.

Alternative function syntax

후행 반환 형식이 도입되었다.
이는 함수 반환 타입을 auto 타입으로 했을 때를 위한 방식으로 도입되었는데, 어떤 타입으로 줘야할지 알지 못하므로 그 타입을 뒤에 명시하는 방식이었다.
그러나 C++14에서는 auto 타입으로 함수를 선언해도 후행 반환을 사용하지 않아도 되도록 업그레이드 되었다. 예를 들면 아래와 같다.

template<class Lhs, class Rhs>
auto adding_func(const Lhs &lhs, const Rhs &rhs) -> decltype(lhs + rhs) {return lhs + rhs;}
// auto adding_func(const Lhs &lhs, cont Rhs &rhs) {return lhs + rhs;}   // C++14

return value는 lhs + rhs의 타입이어야 하는데, lhs와 rhs가 아직 정의되지 않은 상태에서 아래 코드를 사용할 수 없다.
decltype(lhs + rhs) adding_func(const Lhs &lhs, const Rhs &rhs) {return lhs + rhs;}

Object construction improvement

C++03까지는 생성자에서 다른 생성자를 호출할 수 없었다. 따라서 하나의 클래스에 다른 생성자가 동일한 구현이 필요할 경우 꼼수?처럼 함수를 만들어 호출하는 방식을 사용하였으나, C++11부터는 생성자에서 다른 생성자를 호출할 수 있게 되었다.
또한 non-static member의 초기화를 선언부에서 할 수 있게 되었다. 이 말이 뭐냐면 헤더파일에 멤버변수 선언하고, 생성자에서 초기화 하던걸 헤더파일에서 멤버변수 선언할때 초기값을 집어넣을 수 있어서 쓸데 없이 생성자에서 한 번 더쓰던걸 안써도 된다는 말이다.

overrides & final

Java에서 쓰는 애들이랑 유사하다. override와 final을 뒤에 붙이는게 특이하다.
virtual 함수에만 사용할 수 잇다. Java와 마찬가지로 필수로 사용되어야 하는 것은 아니다.

final:

  • 하위 클래스에서 더이상 재정의할 수 없음을 컴파일러에게 알린다.
  • class에도 사용할 수 있다.
    override:
  • 상위 클래스의 멤버함수를 재정의해야 함을 컴파일러에게 알린다.

override의 조건이 더 까다로워 졌기 때문에, 혹시 모를 휴먼 에러를 피하고자 override를 명시하는 것을 권장한다.
override 조건:

  1. parent class의 함수가 virtual.
  2. 함수의 이름이 동일.
  3. 매개변수 현식들이 동일.
  4. const 여부가 동일.
  5. return 형식 동일.
  6. 예외 명세가 호환되어야 함.
  7. 함수들의 참조(reference) 한정사가 동일 // C++11에서 추가
    • 새로 추가된 한정사를 활용해 한정사가 다를 때마다 다른 함수나 생성자를 사용하도록 할 수 있지만, 잘 모르는 경우 이거 때문에 문제를 만들 수도 있음.

null pointer constant

기존의 NULL을 대체하기 위해 nullptr이 생겼다.
auto와 함께 했을 때, 타입을 알기 어려울 때 nullptr이 더 효율적으로 쓰인다.
사실 NULL0도 pointer가 nullptr이라는 것을 표현하기에 정확하지 않았고, NULL이 0으로 정의되어 있기 때문에 아래와 같은 문제점이 발생하기도 하였음.

void f(int n);
void f(char* p);

f(NULL);  // pointer 호출 의도로 f(p)를 의도했으나 f(n)이 호출됨.

nullptr은 새로 추가된 smart pointer에서도 사용되는 것으로 보임. 기존의 코드를 업그레이드 한다면, null 체크하는 부분과 초기 값 등을 모두 nullptr로 바꿔야 문제가 생기지 않을 것.

enum class

자료형에 안전한 enum class가 생성되었다. 기존의 enum은 자료형과 상관 없는 아이들이었다.
기존의 enum은 Type에 상관 없이 int type으로 취급받아 숫자 혹은 다른 타입과 비교가 가능했다.
추가된 enum class는 이런 Type에 안전하다.(유연성은 좀 떨어져도)
enum이 업그레이드 된게 아니라, enum class가 추가된 것으로 기존의 enum은 그대로 사용할 수 있다.

// 기존의 enum
enum Number {ZERO, ONE, TWO}
int n = ZERO;   // OK

// enum class
enum class Number {ZERO, ONE, TWO}
int n = ZERO;             // compile error. ZERO is not exist.
Number n = ZERO;          // compile error. also.
Number n = Number::ZERO;  // OK
int n = Number::ZERO;     // compile error. Number can't be int.
int n = static_cast<int>(Number::ZERO); // OK

class 명시까지 해서 좀 길어졌다고 생각할 수도 있지만, enum이 굉장히 많이 쓰이는 큰 프로그램에서는 중복 사용을 피하기 위해 KEY1_KEY2_KEY3_VALUE 와 같이 사용하던 것에 비해 훨씬 안전하고 효율적이어졌다.

템플릿 괄호처리

C++03까지 >>은 항상 shift 연산자로 판단되어 왔지만, C++11은 템플릿에서 >를 잘 판별할 수 있게 되었다.

vecotr<pair<int,int> > v;   // C++03 >>를 못써서 이렇게
vector<pair<int,int>> v;    // C++11 >>사용 가능

Explicit conversion operators

Cpp Docs 참고.
형 변환 연산자(conversion operator)로서 explicit을 사용할 수 있게 되었다. 딱 명시적으로 타입을 말해야만 변환이 된다.

Template aliases

using을 통해 기존의 typedef의 역할을 더 강력하게 지원할 수 있음.
typedef는 template과 사용할 수 없었는데, using은 template과 사용할 수 있음.
또 기존에 typedef가 하던 것도 더 이쁘장하게 가능함.

typedef void (*FuncionType)(double);    // old style
using FunctionType = void (*)(double);  // new introduce

template<typename First, typename Second, int Third>
class SomeType;

template<typename Second>
typedef SomeType<OtherType, Second, 5> TypedefName;   // Invalid in typedef
using TypedefName = SomeType<OtherType, Second, 5>;   // valid in using

Variadic templates

가변인자 함수를 지원하던 것처럼, 가변 템플릿을 지원함.
C++03에서는 템플릿에서 가변 인자를 구현하려면 직접 지원하고 싶은 인자 수만큼 템플릿을 정의해주어야 했음.
가변인자 함수와 동일하게 사용할 수 있음.

New String Literal

이전에는 unicode encoding의 string을 지원하지 않았는데, UTF-8, -16, -32의 인코딩을 지원함.

u8"I'm a UTF-8";
u"This is a UTF-16";
U"This is a UTF-32";

또한 raw string literal을 위한 아래와 같은 리터럴도 제공함.

R"(The String Data \ stuff")"
R"delimiter(The String Data \ Stuff")delimiter"

User-defined Literal

MS Docs 참고.
기존의 float f = 10f와 같이 선언하는 것처럼, 리터럴을 맘대로 추가할 수 있게 되었음.
이를 통해 단위 변환, 환산 가중치 연산도 간단하게 가능해졌음. 예제 참고.

Multithreading memory model

C++11에서 multithread programming을 지원하기 위한 메모리 모델과 라이브러리를 지원.
std::thread인데, 초기엔 멀티 플랫폼에서 제대로 동작하지 않는다는 말이 많았으나, 최근에는 잘 지원하는 것으로 보임.

TLS

Thread Local Storage의 약자. java의 ThreadLocal 처럼, 스레드에 따라 다른 값을 갖도록 하는 변수를 선언.

default & delete

default:
default construcotr는 컴파일러가 기본적으로 생성하는 생성자인데, 이미 다른 생성자가 생성되어 있다면 컴파일러는 기본 생성자를 생성하지 않는다.
이런 상황에서 생성자 없이 객체를 명시만 하는 경우 오류가 발생하는데 이를 막기 위해 default constructor를 생성해야 하며, 이를 간단하게 해주는 것.
정의와 구현이 나뉘어 있는 경우, 이를 사용하여 구현을 따로 하지 않아도 됨.

struct NonDefault{
    // NotDefault() = default;
    NonDefault(int i);
}

NotDefault n;  // default constructor가 없으므로 에러.

default관련 stackoverflow 참고.

  • default를 쓰면 trival과 POD이지만, (){}같이 default를 직접 구현하면 trival도 POD도 아니게 됨.
    ```cpp struct X{ X() = default; }; struct Y{ Y(){}; };

printf(“%d %d\n”, std::is_trivial::value, std::is_pod::value); printf("%d %d\n", std::is_trivial::value, std::is_pod::value);


delete:  
**delete**는 해당 기능을 사용할 수 없게 만든다.  
```cpp
// copy가 불가능한 클래스 생성
struct NonCopyable {
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
};

// f에서 inst를 매개변수로 받을 수 없도록
struct NoInt {
    void f(double i);
    void f(int) = delete;
}

// f에서 double만 사용할 수 있도록
struct OnlyDouble {
    void f(double d);
    template<class T> void f(T) = delete;
};

C++03까지는 이런 코드들을 delete가 아닌 private으로 만들고, 구현부를 정의하지 않음으로써 사용하지 못하도록 했는데, 이젠 delete를 사용할 수 있음.
이전엔 링킹 시점에서 오류를 발견했다면, delete를 사용하면 컴파일 시점에서 오류를 발견할 수 있음.

long long

long long (int), unsigned int, … char16_t 등 새로운 자료형이 도입됐다.
응? 얘네가 C++11에서야 들어온 애들이었구나..

static assertion

컴파일 타임에 assertion 테스트를 할 수 있는 static assert가 도입되었다.
template 같은 경우의 assertion 테스트가 용이하다.

sizeof member

클래스 멤버에 대한 sizeof를 허용한다는 것인데.. 내 리눅스에서는 왜 C++03으로 되는지 의문..

struct SomeType { OtherType member; };
sizeof(SomeType::member);   // not work with C++03

alignas & alignof

MS Docs 참고.
메모리 주소를 맞춰주는 역할을 하는 alignas와 그 크기를 찾는 alignof가 새로 추가 됨.
지정한 타입이 메모리의 어느 위치에 배치되는지를 확인하는 것.

struct alignas(16) Bar {
    int i;       // 4 bytes
    int n;      // 4 bytes
    alignas(4) char arr[3];
    short s;          // 2 bytes
};

std::cout << alignof(Bar) << std::endl; // output: 16

Attributes

attribute가 도입되었는데, #pragma등으로 사용되던 extension들과 유사하게 제공하는 표준.
[[ ]]과 같은 형태로 도입되었고, standard attribute로 noreturncarries_dependency가 도입되었음.

noreturn stackoverflow 참고.
[[noreturn]] void f()의 의미는 return 값이 없다는게 아니라, call flow를 caller에게 보내지 않는다는 것이다.

  • f() 내에서 exit, loop forever, throw exception 같은 상황이 발생할 경우이고, 이럴때 컴파일러에서의 최적화와 경고 출력을 하기 위함. 함부로 쓰기에는 위험.

carries_dependency는 함수에 데이터 종속성을 지정하여 멀티스레드 동기화 관련해 효율적일 수 있게 함.

tuple type

구조체의 일반화로 볼 수 있는데, Variadic templates를 활용하여 생성되었음.

template <class... Types> class tuple;

default constructor가 있는 클래스들만으로 tuple을 만들면 내부의 다른 정의 없이 사용할 수 있고, 다른 동일한 tuple로 할당하는 것도 가능함.

typedef std::tuple<int, double, const char *> test_tuple;
test_tuple proof(18, 6.5, "AAA");
int i = std::get<0>(proof);

regular expression

#include <regex>;
std::regex rgx("\\n");

과 같이 사용할 수 있으나, 성능이 굉장히 안좋다고 함. 자바의 regular 보다도 훨씬 엄청나게 안좋다고 함. (그럼 왜 만든거…)

smart pointer

C++11에 추가된 unique_ptr, shared_ptr, weak_ptr을 기반으로 한 포인터.
꾸준히 업데이트 되고 있는 걳으로 보이나, 기존의 포인터와 호환하여 사용할 수 없음.

unique_ptr<int> p = make_unique<int>(5); // smart pointer
printf("%d", *p); // use as normal pointer
int a = 1;
p = &a; // error

기존의 단점:

  1. 딱 보면 포인터인지 모름.
  2. delete 해야하는지의 여부를 모름.
  3. delete 해야하더라도 delete []를 해야하는지 여부를 모름.
  4. 코드 상에서 delete 해주는 함수가 이미 구현되어 있는지 여부도 모름.
  5. 따라서 실수가 많아짐. (가비지 컬렉션 관련된)

smart pointer:

  1. pointer를 감싸주는 일종의 wrapper.
  2. pointer와 비슷하게 동작하며 여러 위험을 피할 수 있게 함.
  3. 생? pointer가 할 수 있는 대부분의 일을 하며 알아서 소멸 됨.
  4. 당연히 코드가 줄어듦.

unique_ptr:
독점적 소유권. 포인터를 이동은 가능하나, 여러 포인터가 가르킬 수 없음. shared_ptr로 변환 가능.

shared_ptr:
소유권이 나뉘며 가리키는 포인터가 늘어날 때마다 카운트가 증가하고, 카운트가 0이 될 때 소멸함.

weak_ptr:
shared에서 서로 가리켜서 삭제되지 않는 경우에 대한 문제를 해결하기 위해 고안 됨.