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_map 및 unordered_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;
}