본문 바로가기
프로그래밍/C & C++

[C++] C++11에서 추가된 기능 (1)

by 별준 2022. 12. 3.

References

  • The C++ Standard Library

Contents

  • nullptr and std::nullptr_t
  • auto
  • Uniform Initialization
  • Range-based Loop
  • Move Semantics and Rvalue References
  • New String Literals
  • Keyword noexcept
  • Keyword constexpr

C++11부터 유용하고 새로운 기능들이 추가되었는데, references에서 언급하고 있는 C++11 기능들을 이번 포스팅을 통해서 한 번 정리해보고자 합니다.

Spaces in Template Expressions

std::vector<std::list> >;   // OK in each C++ version
std::vector<std::list>>;    // OK since C++11

nullptr and std::nullptr_t

C++11부터는 0 또는 NULL 대신 nullptr을 사용할 수 있습니다. 이는 포인터가 아무런 값도 가리키고 있지 않다는 것을 지칭하며, undefined value를 가지는 것과는 구분됩니다. nullptr을 사용하면 아래와 같이 의도치 않게 null pointer가 정수값으로 처리되는 것을 피할 수 있게 해줍니다.

void f(int);
void f(void*);

f(0);       // calls f(int)
f(NULL);    // calls f(int) if NULL is 0
f(nullptr); // calls f(void*)

nullptr은 C++11의 새로운 keyword이며, 자동으로 정수 타입이 아닌 각 포인터 타입으로 변환합니다. nullptr은 실제로 <cstddef>에 정의되어 있는 std::nullptr_t 타입이며 기본 데이터형입니다. 또한, null pointer를 파라미터로 전달해야 하는 연산을 오버로딩할 수 있습니다.

 

자동 타입 추론 (auto)

C++11부터 auto 키워드를 사용하여 특정 타입을 지정하지 않아도 변수나 객체를 다음과 같이 선언할 수 있습니다.

auto i = 42;   // i has type int
double f();
auto d = f();  // d has type double

auto는 initializer로부터 타입을 추론하기 때문에 초기화없이 사용할 수 없습니다.

또한, 아래 예시와 같이 타입이 길거나 복잡한 경우에 유용하게 사용될 수 있습니다.

std::vector<std::string> v;
...
auto pos = v.begin();  // pos has type std::vector<std::string>::iterator

auto l = [](int x) -> bool { ... }; // l has the type of a lambda

 

Uniform Initialization and Initializer Lists

C++11 이전에는 변수나 객체를 초기화하는 방법을 헷갈리기 쉬웠다고 합니다. 초기화하는 방법으로는 ()나 {}, 또는 assignment operator를 사용하는 방법들이 있습니다. 이러한 이유로 C++11에서는 초기화 방법을 통일(uniform initialization)하였고, 이제는 단 하나의 공통된 문법을 사용할 수 있는데, 바로 {} 를 사용하는 것입니다.

int values[] { 1, 2, 3 };
std::vector<int> v { 2, 3, 5, 7, 11, 13, 17 };
std::vector<std::string> cities {
    "Berlin", "New York", "London", "Braunschweig", "Cairo", "Seoul"
};
std::complex<double> c{4.0, 3.0}; // c(4.0, 3.0)과 동일

 

Initializer list는 value initialization이라고 불리는 동작을 수행합니다. undefined initial value를 갖는 기본 데이터 타입인 지역 변수는 0으로 초기화되고, 포인터인 경우에는 nullptr로 초기화됩니다.

int i;     // i has undefined value
int j{};   // j is intialized by 0
int* p;    // p has undefined value
int* q{};  // q is initialized by nullptr

 

단, {} 내에서 narrowing initialization (정밀도를 줄이거나, 명시한 값을 수정하는 것)은 불가능합니다. 아래의 예제를 살펴보겠습니다.

int x1(5.3);       // OK, but x1 becomes 5
int x2 = 5.3;      // OK, but x2 becomes 5
int x3{5.0};       // ERROR: narrowing
int x4 = {5.3};    // ERROR: narrowing
char c1{7};        // OK: even though 7 is an int, this is not narrowing
char c2{99999};    // ERROR: narrowing (if 99999 doesn't fit into a char)
std::vector<int> v1 { 1, 2, 4, 5 };       // OK
std::vector<int> v2 { 1, 2.3, 4, 5.6 };   // ERROR: narrowing doubles to ints

위의 코드에서 볼 수 있듯이, 컴파일 시간에 현재 값을 알 수 있다면 narrowing을 적용할 수 있는지 검사합니다. 비야네 스트롭스트룹은 이와 같은 예시에 대해서 다음과 같이 언급하고 있습니다.

무엇이 narrowing conversion인지 결정할 때 발생할 수 있는 많은 불일치를 피하기 위해 C++11은 가능한한 initializers의 실제 값(위의 예시에서 7과 같은 값; line 5)에 의존한다. 만약 값이 표현하는 것이 정확히 target type과 일치한다면, 해당 변환은 narrowing이 아니다. 부동소수점에서 정수로의 변환은 7.0을 7로 바꾸더라도 항상 narrowing이다.

 

사용자 정의 타입에 initializer list를 지원하기 위해, C++11은 std::initializer_list<> 클래스 템플릿을 제공합니다. 이 템플릿은 값 목록(a list of values)이나 값 목록을 위치시키는 것만으로 초기화하고 싶을 때 쓸 수 있습니다.

void print(std::initializer_list<int> values)
{
  for (auto p = values.begin(); p != values.end(); p++) {
    std::cout << *p << "\n";
  }
}

print({12, 3, 5, 7, 11, 13, 17}); // pass a list of values to print()

 

만약 아래의 클래스와 같이 인자와 initializer list를 받는 생성자가 모두 있다면, initializer list를 사용하는 생성자가 선택됩니다.

class P
{
public:
  P(int, int);
  P(std::initializer_list<int>);
};

P p(77, 5);     // calls P::P(int,int)
P q{77, 5};     // calls P::P(std::initializer_list<int>)
P r{77, 5, 42}; // calls P::P(std::initializer_list<int>)
P s = {77, 5};  // calls P::P(std::initializer_list<int>)

위 클래스에서 initializer lists를 받는 생성자가 없다면, P 클래스의 생성자는 q와 s를 초기화하기 위해 2개의 int를 받는 생성자를 호출하게 됩니다. r은 더이상 유효하지 않게 됩니다.

 

이러한 initializer lists 때문에 이제 하나 이상의 인자를 받는 생성자에서도 explicit을 사용할 수 있게 되었으며, 여러 값들이 자동 형변환되는 것을 막을 수 있습니다. 이 방법은 '=' 문법을 사용하여 초기화할 때도 사용됩니다.

class P
{
public:
  P(int a, int b) {
    ...
  }
  explicit P(int a, int b, int c) {
    ...
  }
};

P x(77, 5);      // OK
P y{77, 5};      // OK
P z{77, 5, 42};  // OK
P v = {77,5};    // OK (implicit type conversion allowed)
P w = {77,5,42}; // ERROR due to explicit (no implicit type conversion allowed)

void fp(const P&);

fp({47,11});    // OK, implicit conversion of {47,11} into P
fp({47,11,3});  // ERROR due to explicit
fp(P{47,11});   // OK, explicit conversion of {47,11} into P
fp(P{47,11,3}); // OK, explicit conversion of {47,11,3} into P

 

Range-Based for Loops

C++11에서는 다른 프로그래밍 언어에서 사용되는 foreach 루프와 동일한 형태의 for 루프를 도입하여 주어진 범위의 배열이나 콜렉션(collection)의 모든 요소를 반복할 수 있도록 했습니다. 일반적인 문법은 다음과 같습니다.

for (decl : coll) {
  statement;
}

위의 코드에서 decl은 전달된 콜렉션 coll의 각 요소들에 대한 declaration이고, 각 decl에 대해 statement가 수행됩니다. 아래 예제 코드는 전달된 initializer list의 각 요소에 대해 지정된 구문(cout 출력)을 수행합니다.

for (int i : {2, 3, 5, 7, 9, 13, 17, 19}) {
  std::cout << i << std::endl;
}

벡터의 요소에 각 3을 곱하고 싶으면 다음과 같이 작성하면 됩니다.

std::vector<double> vec;
...
for (auto& elem : vec) {
  elem *= 3;
}

여기서 벡터 요소들을 reference로 선언하지 않으면 local copy로 동작하기 때문에 여기서는 reference를 사용했습니다.

 

각 요소에 대해 복사 생성자와 소멸 생성자를 호출하지 않도록 하려면, 요소를 상수 참조자(constant reference)로 선언해야 합니다. 따라서, 콜렉션에 대한 모든 요소를 출력하는 일반적인 함수는 다음과 같이 구현할 수 있습니다.

template<typename T>
void printElements(const T& coll)
{
  for (const auto& elem : coll) {
    std::cout << elem << std::endl;
  }
}

위와 같은 range-based for 구문은 아래와 동일합니다.

for (auto _pos = coll.begin(); _pos != coll.end(); _pos++) {
  const auto& elem = *_pos;
  std::cout << elem << std::endl;
}

 

일반적으로 range-based for loop는 다음과 같이 선언되고,

for (decl : coll) {
   statement;
}

만약 coll이 begin()과 end() 멤버 함수를 제공한다면, 아래의 코드와 동일합니다.

for (auto _pos = coll.begin(); _pos != coll.end(); _pos++) {
      decl = *_pos;
      statement;
}

만약, 일치하는 begin()과 end()가 없다면 coll을 인자로 받는 global begin()과 end()를 사용합니다.

for (auto _pos = begin(coll); _pos != end(coll); _pos++) {
  const auto& elem = *_pos;
  std::cout << elem << std::endl;
}

클래스 템플릿인 std::initializer_list<>는 begin(), end() 멤버 함수를 제공하기 때문에 initializer list으로 range-based for loop 사용할 수 있습니다.

 

또한, 크기가 알려진 C 스타일 배열에 대해서도 range-based for loop를 사용할 수 있습니다.

int array[] = { 1, 2, 3, 4, 5 };
long sum = 0;
for (int x : array) {
  sum += x;
}

for (auto elem : { sum, sum * 2, sum * 4 }) {
  std::cout << elem << std::endl;
}

 

참고로 for 루프 내에서 요소들일 decl로 초기화될 때에는 어떠한 명시적인 데이터 변환도 사용할 수 없습니다. 따라서 아래의 코드는 컴파일되지 않습니다.

class C
{
public:
  explicit C(const std::string& s); // explicit(!) type conversion from string
  ...
};

std::vector<std::string> vs;
for (const C& elem : vs) { // ERROR, no conversion from string to C defined
  std::cout << elem << std::endl;
}

 

Move Semantics and Rvalue References

C++11에서 추가된 가장 중요한 특징 중 하나는 바로 move semantics 입니다. 이는 C++의 주요 설계 목적인 불필요한 복사나 임수 변수를 없애는데 크게 기여했습니다.

 

해당 내용은 상당히 복잡하기 때문에 참고 서적에서는 일단 간단히 요약만 하고 있습니다.

먼저 아래의 예제 코드를 살펴보겠습니다.

void createAndInsert(std::set<X>& coll)
{
  X x; // create an object of type X
  ...
  coll.insert(x); // insert it into the passed collection
}

위 코드는 새로운 객체를 콜렉션에 삽입합니다. 콜렉션은 전달된 요소의 internal copy를 생성하는 멤버 함수를 제공합니다.

namespace std {
  template<typename T, ...> class multiset {
  public:
    ... insert (const T& v); // copy value of v
    ...
  };
}

이러한 동작은 요소를 삽입한 뒤에도 사용되고 수정될 수 있는 객체나 임시 객체를 삽입할 때 유용합니다.

X x;
coll.insert(x); // insert copy of x
...
coll.insert(x + x); // insert copy of temporary rvalue
...
coll.insert(x); // insert copy of x (although x is not used any longer)

위 코드에서 마지막 두 번의 insert에서는 호출자가 인자로 전달된 값(x+x의 결과와 x)을 더 이상 사용하지 않기 때문에 coll이 internal copy를 만들지 않고 원본을 새로운 요소로 그대로 이동시키면 좋을 것입니다. 특히 x의 복사 비용이 큰 경우(ex, 큰 문자열 콜렉션인 경우) 이것만으로도 성능이 크게 향상될 수 있습니다.

 

C++11부터는 이러한 동작을 지원합니다. 하지만, 임시 변수(객체)가 사용되지 않는 경우에는 프로그래머가 move가 가능하다는 것을 명시해야 합니다. 일반적인 경우에는 컴파일러가 이러한 상황을 캐치할 수 있지만, 프로그래머가 이 기능을 수행하도록 하면 논리적으로 적절한 모든 곳에서 move semantic을 사용할 수 있습니다.

바로 위의 코드는 아래와 같이 간단히 수정할 수 있습니다.

X x;
coll.insert(x); // insert copy of x (OK, x is still used)
...
coll.insert(x + x); // moves (or copies) contents of temporary rvalue
...
coll.insert(std::move(x)); // moves (or copies) contents of x into coll

<utility>에 선언된 std::move()를 사용하면 x는 copy가 아닌 move 동작을 수행합니다. 그러나 std::move() 자체는 이동과 관련된 어떠한 동작도 수행하지 않으며, 단순히 전달받은 인자를 rvalue reference(우측값 참조)로 변환할 뿐입니다. rvalue reference는 X&&과 같이 두 개의 &로 선언된 데이터 타입을 의미합니다. 이 새로운 데이터 타입은 수정될 수 있는 ravlues (할당문에서 오른쪽에만 위치할 수 있는 anonymous temporaries)를 나타냅니다. rvalue는 더 이상 필요하지 않은 (temporary) 객체이므로 이 객체의 contents나 resources를 steal할 수 있다고 봅니다.

 

이제 위에서 살펴본 insert를 오버로딩하여 rvalue references를 처리해보도록 하겠습니다.

namespace std {
  template<typename T, ...> class multiset {
  public:
    ... insert (const T& v); // for lvalues: copies the value
    ... insert (T&& x);      // for rvalues: moves the value
    ...
  };
}

rvalue references에 대한 insert() 함수는 x의 contents를 훔칠 수 있어 성능을 최적화할 수 있습니다. 하지만 이를 위해서는 x의 데이터 타입의 도움이 필요한데, x의 데이터 타입만 자신의 내부에 접근할 수 있기 때문입니다. 예를 들어, 삽입된 요소를 초기화하는 x의 내부 배열이나 포인터를 사용할 수 있다면, 클래스 X 자체가 매우 복잡한 타입일 때 element-by-element copy를 하지 않아도 되어 굉장한 성능 향상을 얻을 수 있습니다. 이때 내부 요소를 초기화할 때 클래스 X의 이동 생성자(move constructor)만 간단히 호출하면 전달된 인자의 값을 새로운 객체로 복사가 아닌 이동으로 초기화할 수 있습니다. 따라서, 이동 생성자를 제공하는 데이터 타입이 필요합니다.

class X {
public:
  X(const X& lvalue); // copy constructor
  X(T&& rvalue);      // move constructor
};

만약 문자열을 위한 이동 생성자라면 새로 배열을 생성하고 모든 요소를 복사하는 대신, 원본 문자열 배열을 새로운 객체에 할당만하면 됩니다. 모든 콜렉션 클래스에 동일하게 적용되며, 모든 요소에 대한 복사본을 만드는 대신 새로운 객체에 원본의 메모리만 전달하게 됩니다.

만약 이동 생성자가 지원되지 않는다면, 복사 생성자가 대신 사용됩니다.

 

이때, 값을 전달한 원본 객체에 어떠한 작업을 수행해도 새로운 객체에 영향을 미치지 않도록 주의해야 합니다. 즉, 전달된 원본 객체의 내부는 깨끗하게 비워주어야 합니다. 예를 들어, 자신이 가지고 있던 요소를 가리키는 내부 포인터는 이동 시킨 후에는 nullptr을 가리키도록 하는 것이 좋습니다.

엄밀히 말하면, move semantics에서 객체의 내용을 비우는 것이 필수는 아니지만, 비우지 않는다면 거의 모든 메커니즘이 의미가 없게 됩니다. C++ 표준 라이브러리의 클래스의 경우, 이동한 후 내부가 비워지는 것을 보장하며, 해당 객체는 유효하지만 undefined state가 됩니다. 즉, 이동된 후 해당 객체에 새로운 값을 할당할 수는 있지만 현재 값은 undefined라는 것입니다. STL 컨테이너의 경우 값이 이동된 컨테이너는 이후에 빈 상태를 유지한다는 것이 보장되어 있습니다.

 

동일한 방식으로 모든 자명하지 않은 클래스들은 복사 할당 연산자(copy assignment operator)와 이동 할당 연산자(move assignment operator)를 모두 제공해야 합니다.

class X {
public:
  X& operator=(const X& lvalue); // copy assignment operator
  X&& operator=(X&& rvalue);     // move assignment operator
  ...
};

문자열이나 콜렉션의 경우 이러한 연산자는 간단한 내부 contents와 resources에 대한 swap으로 구현될 수 있습니다. 이때, 객체가 lock과 같은 리소스를 가지고 있다면, 객체의 내용을 깨끗히 비워 빨리 릴리즈하는 것이 더 좋습니다. move semantics에서 이러한 동작이 필수로 요구되는 것은 아니지만, C++ 표준 라이브러리의 컨테이너 클래스들은 이 정도의 이동 연산은 지원합니다.

 

Overloading Rules for Rvalue and Lvalue References

rvalue와 lvalue references에 대한 오버로딩 규칙은 다음과 같습니다.

 

  • 만약 void foo(X&&) 없이 void foo(X&)만 구현한다면, C++98과 동일하게 동작합니다. foo() 함수는 lvalue에 대해 호출될 수 있지만 rvalue에 대해서는 호출되지 않습니다.
  • 만약 void foo(X&&) 없이 void foo(const X&)만 구현한다면, 마찬가지로 C++98과 동일하게 동작합니다. 이때, foo() 함수는 lvalue와 rvalue 모두에 대해 호출될 수 있습니다.
  • void foo(X&), void foo(X&&) 또는 void foo(const X&), void foo(X&&) 를 구현한다면 rvalue와 lvalue를 처리하는 방식을 구분할 수 있습니다. rvalue에 대한 버전은 move semantics를 허용하고 지원해야 합니다.
  • 만약 void foo(X&&) 함수만 구현하고, void foo(X&)와 void foo(const X&)는 구현하지 않는다면 foo() 함수는 rvalue에 대해서만 호출될 수 있으며 lvalue에 대해서 호출할 경우 컴파일 에러가 발생합니다. 따라서 이러한 경우에는 move semantics만이 제공됩니다. 이러한 기능은 unique pointer, file streams, string streams 등의 라이브러리에서 사용됩니다.

위 규칙에 따라서, 만약 클래스가 move semantics를 지원하지 않고 일반적인 복사 생성자와 복사 할당 연산자만을 가지고 있다면 rvalue references에 대해서 이들 함수가 호출됩니다. 즉, std::move()를 사용하여 인자에 전달하더라도 move semantics을 지원하지 않으면 copy semantics로 동작합니다.

 

Returning Rvalue References

move()는 값을 반환할 필요도 없으며, 반환해서도 안됩니다. C++ language rules에 따르면, 표준은 아래의 코드에 대해

X foo()
{
  X x;
  ...
  return x;
}

아래의 동작이 보장된다고 명시하고 있습니다.

  • 만약 X가 copy 또는 move constructor를 제공한다면, 컴파일러는 복사를 생략할 수 있습니다. 이는 (named) return value optimization((N)RVO, 반환값 최적화)라고 불리며, 이 기능은 C++11에서 명시하기 전에도 많은 컴파일러에서 지원하는 최적화 기능입니다.
  • 그렇지 않고, X에 move constructor가 있다면 x는 이동됩니다.
  • 그렇지 않고, X에 copy constructor가 있다면 x는 복사됩니다.
  • 위의 케이스에 포함되지 않으면, 컴파일 에러가 발생합니다.

만약 반환되는 객체가 local nonstatic 객체라면 rvalue reference를 반환하는 것은 에러를 발생시킵니다.

X&& foo()
{
  X x;
  ...
  return x; // ERROR: returns reference to nonexisting object
}

rvalue reference도 reference이므로 local object를 참조하는 rvalue reference를 반환한다는 것은 존재하지 않는 객체에 대한 참조자를 반환한다는 의미입니다. 이는 std::move()의 사용 유무와는 상관없습니다.

 

New String Literals

C++11부터 raw string literals와 multibyte/wide-character string literals을 정의할 수 있습니다.

Raw String Literals

Raw string을 사용하면 원래의 문자열 시퀀스를 그대로 유지하는 문자열을 정의할 수 있습니다. 따라서, 특수 문자를 사용할 때 추가해주어야 했던 이스케이프 문자를 생략할 수 있어서 훨씬 더 깔끔하게 표현할 수 있습니다.

Raw string은 R"( 로 시작하고 )" 로 끝납니다. 문자열 안에는 줄바꿈(line break)가 들어갈 수도 있습니다. 예를 들어, 두 개의 역슬래시와 n을 포함하는 문자열 리터럴이라면 원래는 다음과 같이 정의해야 했습니다.

"\\\\n"

하지만 raw string을 사용하면 다음과 같이 정의하면 됩니다.

R"(\\n)"

Raw string 내에서 )" 를 사용하고 싶다면, 구분자(delimiter)를 사용하면 됩니다. 실제로 raw string의 원래 문법은 R"delim(...)delim" 이며, 여기서 delim은 역슬래시, 공백과 괄호를 제외한 16개 이하의 기본 문자열이면 됩니다. 예를 들어, 다음과 같이 사용할 수 있습니다.

R"nc(a\
     b\nc()"
     )nc";

위의 raw string을 기존의 문자열로 표현하려면 다음과 같이 작성해야 합니다.

"a\\\n    b\\nc()\"\n    "

Raw string은 정규 표현식을 정의할 때 유용합니다.

 

Encoded String Literals

encoding prefix를 사용하면 문자열 리터럴(string literals)를 위한 특별한 문자 부호화(character encoding)을 정의할 수 있습니다. 다음은 정의된 encoding prefixes 입니다.

  • u8 : UTF-8 encoding을 정의합니다. 정의된 문자들은 const char 타입입니다.
  • u : char16_t 타입을 갖는 문자들로 이루어진 문자열입니다.
  • U : char32_t 타입을 갖는 문자들로 이루어진 문자열 리터럴입니다.
  • L : wchar_t 타입을 갖는 문자들로 이루어진 wide 문자열 리터럴입니다.

예를 들어, 다음과 같이 사용합니다.

L"hello" // wchar_t string literals로 정의

이때, raw string을 사용하려면 encoding prefix 앞에 위치시키면 됩니다.

 

Keyword noexcept

C++11부터는 noexcept 키워드를 제공합니다. 이 키워드는 함수가 예외를 던질 수 없거나 또는 예외를 던질 준비가 되지 않았다고 명시하는데 사용될 수 있습니다.

void foo() noexcept;

위 표현식은 foo() 함수가 앞으로 예외를 던지지 않을 것이라고 선언합니다. 만약 예외가 foo() 내부에서 처리되지 않지만 foo()가 예외를 던진다면, 프로그램은 std::terminate()를 호출하고 이는 기본적으로 std::abort()를 호출하면서 프로그램이 종료됩니다.

noexecpt는 (empty) exception specification을 가진 많은 문제를 해결하기 위해 도입되었습니다.

  • Runtime checking: C++의 exception specification은 컴파일 시간이 아닌 런타임에 검사됩니다. 따라서, 모든 예외가 처리될 것이라고 장담할 수 없습니다. runtime failure mode (std::unexpected()를 호출)은 복구를 맡지 않습니다.
  • Runtime overhead: 런타임에 검사하려면 컴파일러가 추가로 코드를 생성하는데, 이 때문에 최적화가 어려워집니다.
  • Unusable in generic code: 제너릭 코드 내에서는 일반적으로 템플릿 인자에 대한 연산으로부터 어떤 타입의 예외가 발생할 지 예측하기가 쉽지 않습니다. 따라서 정확한 exception specification을 작성할 수 없습니다.

실제 코드에서는 어떤 연산이 예외(any exception)를 던지거나 아니면 예외를 절대로 던지지 않는다라는 두 가지 형태의 exception-throwing에 대한 보장만이 유용합니다. 전자는 exception-specification을 완전히 생략하는 반면, 후자는 throw()로 표현할 수 있지만 성능상의 이유로 거의 사용하지 않습니다.

특히, noexceptSMS stack unwinding(스택 풀기)를 필요로 하지 않기 때문에, 추가적인 부하없이 프로그램이 예외를 던지지 않는다는 보장을 표현할 수 있게 되었습니다. 따라서, C++11 이후로는 exception specification이 폐기될 예정입니다 (C++17부터 완전히 제거되었습니다).

 

어떤 함수가 예외를 던지지 않는다는 조건을 명시할 수도 있습니다. 예를 들어, 어떠한 타입 Type에 대해 global swap()은 보통 다음과 같이 정의됩니다.

void swap(Type& x, Type& y) noexcept(noexcept(x.swap(y))
{
  x.swap(y);
}

여기서, noexcept(...) 내부에 boolean 조건으로 예외를 던지지 않는 조건을 명시할 수 있습니다. 조건없이 noexcept를 명시하는 것은 실제로 noexcept(true)를 줄여서 표현한 것과 같습니다. 위 예제 코드에서는 noexcept(x.swap(y))라는 조건을 사용했으며, 괄호 안의 명시된 표현식이 평가값이 true라면 예외를 던지지 않습니다. 따라서 만약 첫 번째 인자에 대한 멤버 함수 swap()이 예외를 던지지 않는다면 global swap()도 예외를 던지지 않는다고 명시하는 것입니다.

 

또 다른 예시로 value pairs에 대한 이동 할당 연산자를 살펴보겠습니다.

pair& operator=(pair&& p)
        noexcept(is_nothrow_move_assignable<T1>::value &&
                 is_nothrow_move_assignable<T2>::value);

여기서는 is_nothrow_move_assignable이라는 type trait가 사용되어, 전달된 데이터 타입에 따라 이동 연산이 예외를 던지지 않을 수 있는지 확인합니다.

 

Keyword constexpr

C++11부터는 constexpr을 사용하여 컴파일 시간에 표현식을 평가할 수 있습니다. 예를 들면, 다음과 같습니다.

constexpr int square(int x)
{
  return x * x;
}
float a[square(9)]; // OK, since C++11: a has 81 elements

이 키워드는 numeric limits를 사용할 때 발생하는 C++98의 문제점을 고쳤습니다. C++11 이전에는 아래와 같은 표현식은

std::numeric_limits<short>::max()

기능적으로 매크로인 INT_MAX와 동일하지만, 정수형 상수로 사용될 수 없었습니다. 하지만, C++11부터 constexpr 덕분에 아래와 같이 배열을 선언하거나 컴파일 시간 연산(metaprogramming)이 가능하게 되었습니다.

std::array<float, std::numeric_limits<short>::max()> a;

 

 

'프로그래밍 > C & C++' 카테고리의 다른 글

[C++] Pairs and Tuples  (0) 2022.12.06
[C++] C++11에서 추가된 기능 (2)  (0) 2022.12.04
[C/C++] 동적 라이브러리  (0) 2022.11.16
[C/C++] 정적 라이브러리  (0) 2022.11.11
[C/C++] Static vs Dynamic 라이브러리  (0) 2022.11.08

댓글