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

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

by 별준 2022. 12. 4.

References

  • The C++ Standard Library

Contents

  • New Template Features
  • Lambda
  • Keyword decltype
  • New Function Declaration Syntax
  • Scoped Enumerations
  • New Fundamental Data Types
  • +) Old Languages Features

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

 

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

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부터 유용하고 새로운 기능들이

junstar92.tistory.com

지난 포스팅에 이어 C++11에서 도입된 기능들에 대해서 살펴보도록 하겠습니다.

 

New Template Features

Variadic Templates

C++11부터 템플릿에서 가변 길이의 템플릿 인자를 파라미터로 받을 수 있습니다. 이 기능을 variadic templates라고 부릅니다.

void print()
{
}

template<typename T, typename... Types>
void print(const T& firstArg, const Types&... args)
{
  std::cout << firstArg << std::endl; // print first argument
  print(args...);                     // call print() for remaining arguments
}

예를 들어, 위 코드에서 정의된 print 함수에 하나 이상의 인자가 전달되면 템플릿 함수가 사용되어 첫 번째 인수를 분리하고 출력하고 남은 인자들에 대해서 print() 재귀로 호출합니다. 재귀의 끝에서 non-template overload인 print() 함수가 호출됩니다.

print(7.5, "hello", std::bitset<16>(377), 42);

위와 같이 입력하면 아래의 값들을 출력합니다.

아래의 코드는 어떨까요 ?

template <typename T>
void print(const T& arg)
{
  std::cout << arg << std::endl;
}

template<typename T, typename... Types>
void print(const T& firstArg, const Types&... args)
{
  std::cout << firstArg << std::endl; // print first argument
  print(args...);                     // call print() for remaining arguments
}

단일 인자가 가변 형태인지 모호하지만, 일반적으로 컴파일러는 위 코드를 통과시킵니다.

가변 템플릿 내에서 sizeof...(args)는 인자의 갯수를 생성합니다. std::tuple<>은 이 기능을 아주 많이 사용하고 있습니다.

 

Alias Templates (Template Typedef)

C++11에서는 템플릿 (partial) 타입 정의도 지원합니다. 하지만, typename 키워드를 사용한 접근 방법이 여러 이유로 실패했기 때문에 using 키워드를 도입했고, alias template 이라는 용어가 사용됩니다.

template<typename T>
using Vec = std::vector<T, MyAlloc<T>>; // standard vector using own allocator

Vec<int> coll;

 

그 밖에도 함수 템플릿은 기본 템플릿 인자를 가질 수 있고, 또한 local type 또한 템플릿 인자를 가질 수 있습니다.

 

Lambda

C++11부터는 인라인 함수를 선언할 수 있는 람다(lambda)를 도입했습니다. 람다는 파라미터나 로컬 객체로도 사용될 수 있습니다.

 

Syntax of Lambdas

람다는 구문이나 표현식 내에서 정의되로 수 있는 함수의 정의입니다. 따라서, 람다를 인라인 함수로 사용할 수 있습니다. 가장 최소한의 람다는 어떠한 파라미터도 받지 않고 간단히 무언가를 수행하는 것입니다.

[] {
  std::cout << "hello lambda" << std::endl;
}

위의 함수를 아래와 같이 정의함과 동시에 호출할 수 있습니다.

[] {
  std::cout << "hello lambda" << std::endl;
} (); // prints 'hello lambda'

또는 객체에 할당한 뒤, 객체를 호출할 수 있습니다.

auto l = [] {
  std::cout << "hello lambda" << std::endl;
};
...
l(); // prints 'hello lambda'

위에서 람다를 정의할 때, 람다는 lambda introducer(대괄호)라고 불리는 것으로 시작하는데, 람다 내부에서 바깥에 있는 nonstatic 객체에 접근하려면 capture(캡처)라 불리는 것을 대괄호 내에 명시해야 합니다. 외부 데이터에 접근할 필요가 없다면, 위 예시처럼 비워두면 됩니다. std::cout과 같은 정적 객체(static object)는 그냥 사용할 수 있습니다.

 

Lambda introducer와 람다의 body 사이에는 파라미터, mutable, exception specification(deprecated), attribute specifiers, return type을 명시할 수 있습니다. 예를 들어, 다음과 같이 정의할 수 있습니다.

[...] (...) mutable(opt) throwSpec(opt) -> retType(opt) {...}

언급했듯이, 람다는 다른 함수들처럼 파라미터를 가질 수 있습니다.

auto l = [](const std::string& a) {
  std::cout << s << std::endl;
};
l("hello lambda"); // prints 'hello lambda'

하지만, 람다는 템플릿이 될 수는 없으며 항상 모든 데이터 타입을 명시해주어야 합니다.

 

람다는 값을 리턴할 수 있는데, 리턴 타입을 명시하지 않더라도 반환하는 값으로부터 데이터 타입이 추론됩니다. 예를 들어, 아래의 람다의 리턴 타입은 int 입니다.

[] {
  return 42;
}

리턴 타입을 명시하고 싶다면, 일반 함수를 위해서 C++에 새로 도입된 문법을 사용하여 다음과 같이 명시할 수 있습니다. 아래 람다는 42.0을 반환합니다.

[]() -> double {
  return 42;
}

 

Captures (Access to Outer Scope)

Lambda introducer 내부에서 캡처를 활용하면 외부 영역에 있는 데이터를 인자로 전달하지 않아도 접근할 수 있습니다.

  • [=]는 외부 영역을 값으로 전달한다는 의미입니다. 따라서 람다가 정의된 영역에서 읽을 수 있는 모든 데이터를 읽을 수 있지만 수정할 수는 없습니다.
  • [&]는 외부 영역을 참조로 전달한다는 의미입니다. 람다 외부에서 어떤 변수에 대한 쓰기 작업이 가능하다면, 람다 내부에서도 쓰기 작업을 수행할 수 있습니다.

각각의 객체에 대해 람다 내부에서 값 혹은 참조로 전달받을 것인지 명시할 수도 있습니다. 따라서, 다양한 접근 방식을 조합하여 지정할 수 있습니다.

int x = 0;
int y = 42;
auto qqq = [x, &y] {
  std::cout << "x: " << x << std::endl;
  std::cout << "y: " << y << std::endl;
  ++y; // OK
};

x = y = 77;
qqq();
qqq();
std::cout << "final y: " << y << std::endl;

위 코드의 출력은 다음과 같습니다.

x는 값으로 복사되기 때문에 람다 내부에서 그 값을 수정할 수 없으며, ++x를 작성하면 컴파일 에러가 발생합니다. y는 참조로 전달되었으므로 수정할 수 있습니다. 따라서, y의 최종값이 79가 됩니다.

[x, &y] 대신, [=, &y]로 명시할 수도 있는데, 이렇게 하면 y만 참조로 전달되고 나머지 객체는 모든 값으로 전달됩니다.

 

람다를 mutable로 선언하면 값 전달과 참조 전달을 섞어서 사용할 수 있습니다. 이 경우, 객체는 값으로 전달되지만 람다에서 정의된 함수 객체 내에서는 전달된 값을 수정할 수 있습니다.

int id = 0;
auto f = [id]() mutable {
  std::cout << "id: " << id << std::endl;
  ++id; // OK
};

id = 42;
f();
f();
f();
std::cout << id << std::endl;

결과는 위와 같습니다.

 

람다가 동작하는 방식은 아래의 함수 객체와 유사하다고 볼 수 있습니다.

class {
private:
  int id; // copy of outside id
public:
  void operator()() {
    std::cout << "id: " << id << std::endl;
    ++id; // OK
  }
};

mutable이 존재하기 때문에 operator()는 nonconstant member function으로 정의되었고, 이는 id를 수정할 수 있습니다. 따라서, mutable이 있으면 값으로 전달되긴 하지만 어쨌든 상태를 가질 수 있습니다. 일반적으로는 mutable이 없는 람다가 대부분이므로 operator()는 constant member function이 되고, 값으로 전달된 객체를 읽기만 할 수 있습니다.

 

Type of Lambdas

람다의 타입은 익명의 함수 객체(또는 functor)이며, 람다 표현식마다 고유한 데이터 타입을 갖습니다. 따라서, 해당 데이터 타입을 갖는 객체는 선언하기 위해서는 템플릿이나 auto를 사용해야 합니다. 만약 데이터 타입이 필요하다면, decltype() 을 사용하면 됩니다. 예를 들어, 연관(associative) 또는 비정렬(unordered) 컨테이너에 해시 함수나 정렬 기준으로 람다를 사용한다면, 람다의 데이터 타입이 필요할 수 있습니다.

 

또는 C++ 표준 라이브러리에서 제공하는 std::function<> 클래스 템플릿을 사용하여 함수 프로그래밍을 위한 일반 데이터 타입을 명시할 수도 있습니다. 이 클래스 템플릿이 람다를 반환하는 함수의 리턴 타입을 명시하는 유일한 방법입니다.

#include <functional>
#include <iostream>

std::function<int(int, int)> returnLambda()
{
  return [](int x, int y) {
    return x*y;
  };
}

int main()
{
  auto lf = returnLambda();
  std::cout << lf(6,7) << std::endl; // print 42
}

 

Keyword decltype

새로 도입된 decltype 키워드를 사용하면 텀파일러가 표현식의 데이터 타입을 찾을 수 있습니다. 

std::map<std::string, float> coll;
decltype(coll)::value_type elem;

 

decltype은 리턴 타입을 선언하는 데도 사용될 수 있고, 또 다른 예시로는 메타프로그래밍이나 람다의 데이터 타입 전달에도 사용될 수 있습니다.

 

New Function Declaration Syntax

종종 함수의 리턴 타입은 인자를 처리하는 표현식에 달려 있습니다. 하지만 C++11 이전에는 아래와 같은 코드를 사용할 수 없었습니다.

template<typename T1, typename T2>
decltype(x+y) add(T1 x, T2 y);

이는 return expression에 사용된 객체가 아직 introduced되지 않거나 scope 내에 있지 않기 때문입니다. 하지만 C++11에서는 파라미터 리스트 뒤에 함수의 리턴 타입을 선언할 수 있게 되었습니다.

template<typename T1, typename T2>
auto add(T1, x, T2 y) -> decltype(x+y);

여기서 사용되는 문법은 람다에서 리턴 타입을 선언하는 방식과 동일합니다.

 

Scoped Enumerations

C++11은 scoped enumerations의 정의를 허용합니다. 이는 strong enumerations, 또는 enumeration classes(열거형 클래스)라고도 부릅니다. 새로운 열거형은 기존의 C++ 열거형값보다 더 깔끔하게 구현되었습니다.

 

다음 예시를 살펴보겠습니다.

enum class Salutation : char { mr, ms, co, none };

여기서 중요한 점은 enum 뒤에 class 키워드를 명시했다는 점입니다. 이렇게 작성된 scoped enumerations의 장점은 다음과 같습니다.

  • int로 또는 int로부터의 암시적인 변환이 불가능
  • mr과 같은 값은 열거형이 선언된 scope의 일부가 아니며 Salutation::mr로 사용해야 함
  • 실제 데이터 타입(여기서는 char)을 명시적으로 정의할 수 있으며 그 크기도 고정된다 (char을 생략한다면 기본값은 int)
  • 열거형 타입의 전방 선언(forward declarations)가 가능하며, 새로운 열거값의 데이터 타입만 사용되는 경우, 새로 컴파일할 필요가 없다

참고로 std::underlying_type이라는 type trait를 사용하여 열거형 타입의 실제 데이터 타입을 평가할 수 있습니다.

(표준 예외의 에러 조건 값은 scoped enumerators입니다.)

 

New Fundamental Data Types

아래의 데이터 타입은 C++11에서 새롭게 정의되었습니다.

  • char16_t, char32_t
  • long long, unsigned long long
  • std::nullptr_t

 

Old Language Features

마지막으로 C++98까지 사용되어 왔지만, 헷갈리거나 잘 모르는 기능들에 대해서 살펴보겠습니다.

 

Nontype Template Parameters

데이터 타입 파라미터뿐만 아니라, nontype 파라미터도 사용할 수 있습니다. nontype 파라미터도 타입의 일부로 간주됩니다. 예를 들어, 표준 클래스인 bitset<>에서는 템플릿 인자로 비트 수를 전달할 수 있습니다. 아래의 구문은 두 개의 비트필드를 정의하고 하나는 32비트, 나머지 하나는 50비트를 갖습니다.

bitset<32> flags32;
bitset<50> flags50;

이러한 bitset은 템플릿 인자가 서로 다르기 때문에 데이터 타입도 다릅니다. 따라서, 적절한 데이터 타입 변환을 제공하지 않는 한, 둘을 서로 할당하거나 비교할 수 없습니다.

 

Default Template Parameters

클래스 템플릿은 기본 인자를 가질 수 있습니다. 예를 들어, 아래의 선언은 MyClass라는 클래스를 선언할 때, 하나 또는 두 개의 템플릿 인자를 사용할 수 있습니다.

template<typename T, typename container = vector<T>>
class MyClass;

만약 하나의 인자만을 사용한다면 두 번째 인자에는 기본 파라미터가 사용됩니다.

 

Keyword typename

typename 키워드는 뒤에 따라오는 식별자가 데이터 타입이라는 것을 명시하기 위해 도입되었습니다.

template<typename T>
class MyClass {
  typename T::SubType* ptr;
  ...
};

여기서 typename은 SubType이 클래스 T에서 정의된 타입이라는 것을 명확히 하기 위해서 사용됩니다. 따라서 ptr은 T::SubType이라는 데이터 타입을 가리키는 포인터입니다. typename이 없다면 SubType은 static member를 가리킬 수 있는데, 그러면 T::SubType* ptr은 데이터 타입 T의 SubType이라는 값과 ptr을 곱하라는 의미가 됩니다.

SubType이 데이터 타입이라고 한정했기 때문에, T의 자리에 사용된 어떠한 데이터 타입이라도 내부적으로 SubType이라는 데이터 타입을 제공해야 합니다.

 

예를 들어, 데이터 타입 Q를 템플릿 인자로 사용하려면 Q가 SubType에 대한 내부 데이터 타입 정의를 가지고 있어야 합니다.

class Q {
  typedef int SubType;
  ...
};

MyClass<Q> x; // OK

이 경우, MyClass<Q>의 ptr은 int를 가리키는 포인터이며, SubType은 클래스와 같은 추상 데이터 타입일 수도 있습니다.

 

템플릿의 식별자를 타입으로 한정하기 위해서는 반드시 typename을 사용해야 합니다(데이터 타입이 아니라고 생각되더라도). 즉, typename으로 한정되지 않는 모든 템플릿의 식별자는 C++에서 값으로 취급됩니다.

 

Member Templates

클래스의 멤버 함수도 템플릿이 될 수 있습니다. 하지만 멤버 템플릿은 가상 함수일 수는 없습니다.

class MyClass {
  ...
  template<typename T>
  void f(T);
};

위 코드에서 MyClass::f는 어떠한 데이터 타입의 파라미터도 받을 수 있는 멤버 함수 집합을 선언합니다. 어떤 데이터 타입이라도 f()가 사용하는 모든 연산을 지원한다면 f()의 인자로 전달될 수 있습니다.

 

이러한 특성은 클래스 템플릿의 멤버에 대해 automatic type conversion을 지원하기 위해 자주 사용됩니다. 예를 들어, 아래의 클래스 정의에서 assign()의 인자 x는 호출된 객체와 완전히 같은 데이터 타입을 가져야만 합니다.

template<typename T>
class MyClass {
private:
  T value;
public:
  void assign(const MyClass<T>& x) { // x must have same type as *this
    value = x.value;
  }
  ...
};

여기서 다른 타입으로 automatic type conversion이 제공되더라도, assign() 연산의 객체와 템플릿 타입이 다르면 아래 코드처럼 에러가 발생합니다.

void f()
{
  MyClass<double> d;
  MyClass<int> i;
  
  d.assign(d); // OK
  d.assign(i); // Error, i is MyClass<int>
               // but MyClass<double> is required

이때 멤버 함수에 대해 다른 템플릿 타입을 제공한다면, 이러한 완벽한 일치를 어느정도 완화할 수 있습니다. 멤버 함수 템플릿 인자는 할당할 수 있는 데이터 타입이기만 하면 어떠한 템플릿 타입이라도 될 수 있습니다.

template<typename T>
class MyClass {
private:
  T value;
public:
  template<typename X>               // member template
  void assign(const MyClass<X>& x) { // allows different template types
    value = x.getValue();
  }
  T getValue() const {
    return value;
  }
  ...
};

void f()
{
  MyClass<double> d;
  MyClass<int> i;
  
  d.assign(d); // OK
  d.assign(i); // OK (int is assignable to double)

여기서 assign()의 인자 x는 이제 *this의 데이터 타입과 다릅니다. 따라서, MyClass<>의 private이나 protected 멤버에 직접 액세스할 수는 없습니다. 따라서, 위 예시에서는 getValue()와 같은 함수를 추가하여 사용하고 있습니다.

 

템플릿 생성자(template constructor)는 멤버 템플릿의 특별한 형태입니다. 템플릿 생성자는 보통 객체가 복사될 때, 암묵적인 형 변환을 허용하기 위해 사용됩니다. 템플릿 생성자는 복사 생성자의 암묵적인 선언을 막지 않습니다. 따라서, 타입이 정확하게 일치하면, 암묵적인 복사 생성자가 생성되어 호출됩니다.

template<typename T>
class MyClass {
public:
  // copy constructor with implicit type conversion
  // dose not suppress implicit copy constructor
  template<typename U>
  MyClass(const MyClass<U>& x);
  ...
};

void f()
{
  MyClass<double> xd;
  ...
  MyClass<double> xd2(xd); // call implicitly generated copy constructor
  MyClass<int> xi(xd);     // call template constructor
  ...
}

위 코드에서 xd2의 데이터 타입은 xd의 데이터 타입과 동일하기 때문에 암묵적으로 생성된 복사 생성자로 초기화됩니다. 반면, xi의 데이터 타입은 xd의 데이터 타입과 다르기 때문에 템플릿 생성자를 사용하여 초기화됩니다. 템플릿 생성자를 구현할 때, 만약 기본 생성자의 동작이 원하는 것과 다르다면 기본 생성자를 직접 정의하면 됩니다.

 

Nested Class Templates

nested class 또한 템플릿화될 수 있습니다.

template <typename T>
class MyClass {
  ...
  template<typename T2>
  class NestedClass;
  ...
};

 

Explicit Initialization for Fundamental Types

만약 명시적인 생성자를 인자없이 호출하면 기본 데이터 타입은 모두 0으로 초기화됩니다.

int i1;         // undefined value
int i2 = int(); // initialized with zero
int i3{};       // initialized with zero (since C++11)

이 특성을 사용하면 템플릿 코드를 작성할 때 어떠한 데이터 타입의 값도 특정 기본값을 가지도록 할 수 있습니다. 예를 들면, 아래 코드에서는 x가 기본 데이터 타입인 경우 0으로 초기화될 것이라고 보장합니다.

template<typename T>
void f()
{
  T x = T();
  ...
}

 

main() 정의

C++ 표준에는 오직 다음의 두 가지 main() 정의만 가능합니다.

int main()
{
  ...
}

또는,

int main(int argc, char* argv[])
{
  ...
}

여기서 argv는 char**로도 정의될 수 있으며, 리턴 타입 int는 꼭 필요합니다.

 

여기서 main() 함수는 return 문으로 끝낼 수도 있지만 꼭 필요한 것은 아닙니다. C와 달리 C++은 main문 끝에 암묵적으로 return 0;가 있다고 가정합니다. 즉, return문이 없이 main()이 끝나면 항상 0이 리턴되며 모든 프로그램은 성공했다고 간주합니다.

참고로 main()에서 반환하지 C++ 프로그램을 끝내려면 exit(), quick_exit(), terminate()를 사용할 수 있습니다.

 

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

[C++] Numeric Limits  (0) 2022.12.06
[C++] Pairs and Tuples  (0) 2022.12.06
[C++] C++11에서 추가된 기능 (1)  (0) 2022.12.03
[C/C++] 동적 라이브러리  (0) 2022.11.16
[C/C++] 정적 라이브러리  (0) 2022.11.11

댓글