C++11 주요 기능 및 현대적 문법 활용 가이드

C++ 표준은 지속적으로 진화하여 개발의 효율성과 안전성을 높여왔습니다. 2011 년에 공식화된 C++11 은 기존 C++98/03 에 비해 압도적인 개량을 이룩했습니다. 약 140 여 개의 새로운 기능과 600 가지 이상의 버그 수정이 포함되어 언어로서의 완성도가 크게 향상되었습니다. 이번 글에서는 시스템 및 라이브러리 개발에 필수적인 C++11 의 핵심 문법들을 중심으로 정리하고자 합니다.

1. 통일된 초기화 방식 (Uniform Initialization)

기존 C++98 에서는 구조체나 배열에만 중괄호를 활용한 초기화가 가능했으나, C++11 부터는 모든 객체에 일관된 초기화 방식을 적용할 수 있습니다. 이를 리스트 초기화라고 하며, 중괄호 {} 사용을 권장합니다.

struct Vec3 {
    float x, y, z;
};

void testInitialization() {
    // 기본형 지원
    int count = { 10 };
    
    // 사용자 정의 타입 초기화
    Vec3 origin = { 0.0f, 0.0f, 0.0f };
    
    // 등호 생략 가능
    Vec3 forward { 0.0f, 1.0f, 0.0f };
    
    // 컨테이너 초기화
    std::vector<Vec3> points { 
        {0,0,0}, {1,0,0}, {0,1,0} 
    };
}

이를 지원하는 내부 메커니즘으로 std::initializer_list 클래스가 존재합니다. 이 클래스는 배열을 참조하는 포인터와 크기를 관리하며, 컨테이너의构造函数가 이를 인자로 받을 경우 여러 값을 간편하게 전달받을 수 있습니다.

#include <vector>

int main() {
    // 내부적으로 initializer_list 를 사용하는 컨테이너 생성
    std::vector<int> numbers = { 1, 2, 3, 4, 5 };
    return 0;
}

2. 신규 키워드 도입

2.1 auto

복잡한 타입 선언을 생략하고 컴파일러에게 타입 추론을 맡길 수 있습니다. 변수를 정의할 때 반드시 초기화해야 하며, 이때의 초기식으로부터 실제 타입이 결정됩니다.

2.2 nullptr

기존의 NULL 마크로는 상수 0 혹은 (void *)0 으로 정의되어 혼란을 초래할 수 있었습니다. C++11 의 nullptr 는 명확한 포인터 전용 널 리터럴로, 함수 오버로딩 시 정수와의 혼동을 방지합니다.

void processId(int id);
void processPtr(int* ptr);

int main() {
    processId(0);      // 문제 없음
    processId(NULL);   // NULL 이 0 으로 변환되어 호출됨 (의도와 다를 수 있음)
    processPtr(nullptr); // 명확히 포인터 버전 호출됨
    return 0;
}

2.3 decltype

표현식의 타입을 검사하는 키워드입니다.

double value = 3.5;
decltype(value) nextValue = value * 2; // double 타입 자동 대입

3. STL 과 컨테이너 강화

비동기 검색을 위한 unordered_mapunordered_set이 추가되었습니다. 또한 범용 반복자 대신 범용 for 루프 (range-based for) 를 사용하여 컨테이너 순회를 간결하게 만들었습니다.

std::vector<int> data = { 10, 20, 30 };
// C++11 이전보다 깔끔한 순회 방식
for (const auto& item : data) {
    // 사용
}

4. 우연참조와 이동 시맨틱스

효율성을 위해 불필요한 복사 동작을 제거하기 위한 핵심 기능입니다.

4.1 좌값과 우값

메모리 주소를 가질 수 있고 식별 가능한 데이터는 좌값 (L-value) 이고, 임시값이나 결과를 나타내는 것은 우값 (R-value) 입니다. C++11 에서는 우값을 명시적으로 참조할 수 있는 우참조 (R-value reference)가 도입되었습니다.

int& ref = value;   // 좌값 참조
int&& temp = 100;   // 우참조 (임시 값 바인딩 가능)

// std::move 를 통해 좌값을 우값처럼 다룰 수 있게 함
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1); // v1 의 자원 소유권 이전

4.2 이동 생성자와 이동 대입

깊은 복사가 필요한 클래스에서 리소스의 소유권을 단순히 '빌려주는' 방식으로 구현하면 성능이 극적으로 개선됩니다.

class Buffer {
public:
    char* data;
    size_t size;

    // 이동 생성자: 자원을 가져옴
    Buffer(Buffer&& other) noexcept 
        : data(other.data), size(other.size) {
        other.data = nullptr; // 원본 무효화
        other.size = 0;
    }

    // 이동 대입 연산자
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }
};

4.3 완벽한 전달 (Perfect Forwarding)

함수 템플릿 내부에서 매개변수의 속성 (좌/우) 을 보존하면서 다음 함수로 전달해야 할 때 std::forward 와 결합된 우참조를 사용합니다.

template <typename T>
void wrapper(T&& arg) {
    target(std::forward<T>(arg));
}

5. 클래스 기능 확장

  • default: 생성자나 대입 연산자를 명시적으로 기본 구현되도록 지정합니다.
  • delete: 특정 함수의 생성을 원천 차단합니다. 예를 들어 복사를 막으려면 복사 생성자를 = delete 로 설정하면 됩니다.
  • 멤버 초기화: 멤버 변수 선언 시 기본값을 직접 부여할 수 있습니다.

6. 가변 개수 템플릿 (Variadic Templates)

알 수 없는 개수의 템플릿 인수를 처리할 수 있습니다. ... 연산자를 사용하여 파라미터 팩을 생성하고 전개합니다.

#include <iostream>

// 재귀를 이용한 출력
template <typename First, typename... Rest>
void print(First first, Rest... rest) {
    std::cout << first << " ";
    print(rest...); // 나머지 인자 재귀 호출
}

void print() {} // 종료 조건

int main() {
    print(1, "hello", 3.14); 
    return 0;
}

STL 의 emplace_back, emplace 등이 바로 이 기술을 활용해 요소 생성 시 최적화를 수행합니다.

7. 람다 표현식 (Lambda Expressions)

함수 없이 지역적으로 익명 함수를 정의할 수 있습니다. 클로저 형태로 외부 변수를 캡처할 수 있어 알고리즘 커스터마이징에 유용합니다.

#include <algorithm>
#include <vector>
#include <iostream>

struct Employee {
    std::string name;
    double salary;
};

int main() {
    std::vector<Employee> staff;
    
    // 급여 내림차순 정렬
    std::sort(staff.begin(), staff.end(), [](const Employee& a, const Employee& b){
        return a.salary > b.salary;
    });
    
    return 0;
}

캡쳐 목록에는 값 ([=]), 참조 ([&]), 그리고 수정 허용 ([=] mutable) 옵션을 사용할 수 있습니다.

8. 함수 포장자 (Function Wrappers)

8.1 std::function

함수 포인터, 람다, 멤버 함수 등을 통틀어 하나의 타입으로 취급할 수 있게 해주는 일반화된 콜라블 래퍼입니다.

#include <functional>
#include <iostream>

class Calculator {
public:
    int add(int x, int y) { return x + y; }
};

int main() {
    Calculator calc;
    // 함수 포인터
    std::function<int(int, int)> op1 = std::plus<int>();
    
    // 람다
    std::function<int(int, int)> op2 = [](int a, int b){ return a * b; };
    
    // 멤버 함수 바인딩
    std::function<int(Calculator*, int, int)> op3 = &Calculator::add;
    
    std::cout << op1(10, 5) << std::endl;
    return 0;
}

8.2 std::bind

함수의 인자들을 부분적으로 고정하거나 순서를 변경하여 새로운 Callable 을 생성하는 데 사용됩니다.

#include <functional>
using namespace std::placeholders;

int divide(int n, int d) { return n / d; }

int main() {
    // 첫 번째 인자를 100 으로 고정
    auto half = std::bind(divide, _1, 2); 
    std::cout << half(10) << std::endl; // 5 출력
    return 0;
}

태그: C++ C++11 RvalueReference MoveSemantics LambdaExpression

9월 22일 03:27에 게시됨