C++ std::function vs 함수 포인터 | "성능과 유연성" 완벽 비교
이 글의 핵심
C++ std::function vs 함수 포인터에 대한 실전 가이드입니다.
들어가며: “콜백 함수를 어떻게 저장해야 하나요?"
"람다를 함수 포인터에 저장할 수 없어요”
C++에서 콜백 함수를 저장하는 방법은 함수 포인터와 std::function 두 가지가 있습니다. 각각 성능과 유연성에서 차이가 있습니다.
비유로 말씀드리면, 함수 포인터는 정해진 형태의 전화 한 줄로만 걸 수 있는 유선 전화, std::function은 스마트폰 앱처럼 람다·함수 객체까지 담는 통에 가깝습니다. 유연해질수록 내부에서 간접 호출·저장 비용이 늘 수 있습니다.
언제 std::function을, 언제 함수 포인터를 쓰나요?
| 관점 | 함수 포인터 | std::function |
|---|---|---|
| 성능 | 직접 호출에 가깝게 빠름 | 타입 소거·간접 호출로 오버헤드 |
| 사용성 | 캡처 있는 람다 등 저장 불가 | 람다·멤버 바인딩 등 폭넓게 저장 |
| 적용 시나리오 | 핫패스·C API | 콜백 타입을 통일해야 할 때 |
// 함수 포인터: 빠르지만 제한적
void (*callback)(int) = nullptr;
void myFunc(int x) {
std::cout << x << '\n';
}
callback = myFunc; // ✅ OK
callback(42);
// 람다 (캡처 없음)
callback = { std::cout << x << '\n'; }; // ✅ OK
// 람다 (캡처 있음)
int multiplier = 2;
// callback = [multiplier](int x) { std::cout << x * multiplier << '\n'; }; // ❌ 컴파일 에러
// std::function: 느리지만 유연
std::function<void(int)> func;
func = myFunc; // ✅ OK
func = { std::cout << x << '\n'; }; // ✅ OK
func = [multiplier](int x) { std::cout << x * multiplier << '\n'; }; // ✅ OK (캡처 가능!)
이 글에서 다루는 것:
- std::function vs 함수 포인터 차이
- 성능 비교
- 사용 시나리오
- 실전 선택 가이드
목차
1. std::function vs 함수 포인터 차이
비교표
| 항목 | 함수 포인터 | std::function |
|---|---|---|
| 저장 가능 | 일반 함수, 캡처 없는 람다 | 모든 호출 가능 객체 |
| 람다 캡처 | ❌ | ✅ |
| 함수 객체 | ❌ | ✅ |
| 멤버 함수 | ❌ (복잡) | ✅ |
| 성능 | 빠름 | 느림 |
| 메모리 | 8바이트 | 32바이트 + 힙 할당 가능 |
| 타입 안전성 | 낮음 | 높음 |
| C++ 버전 | 모든 버전 | C++11 이후 |
함수 포인터: 빠르지만 제한적
// 함수 포인터 선언
void (*funcPtr)(int) = nullptr;
// 일반 함수
void printInt(int x) {
std::cout << x << '\n';
}
funcPtr = printInt; // ✅ OK
funcPtr(42);
// 캡처 없는 람다
funcPtr = { std::cout << x * 2 << '\n'; }; // ✅ OK
// 캡처 있는 람다
int multiplier = 3;
// funcPtr = [multiplier](int x) { std::cout << x * multiplier << '\n'; }; // ❌ 컴파일 에러
// error: cannot convert lambda with captures to function pointer
std::function: 느리지만 유연
// std::function 선언
std::function<void(int)> func;
// 일반 함수
func = printInt; // ✅ OK
func(42);
// 캡처 없는 람다
func = { std::cout << x * 2 << '\n'; }; // ✅ OK
// 캡처 있는 람다
int multiplier = 3;
func = [multiplier](int x) { std::cout << x * multiplier << '\n'; }; // ✅ OK
func(42); // 126
// 함수 객체
struct Multiplier {
int factor;
void operator()(int x) const {
std::cout << x * factor << '\n';
}
};
func = Multiplier{5}; // ✅ OK
func(42); // 210
// 멤버 함수
class Calculator {
public:
void add(int x) {
std::cout << "Result: " << x + 10 << '\n';
}
};
Calculator calc;
func = [&calc](int x) { calc.add(x); }; // ✅ OK
func(42); // Result: 52
2. 성능 비교
벤치마크: 함수 호출
#include <benchmark/benchmark.h>
#include <functional>
// 테스트 함수
int add(int a, int b) {
return a + b;
}
// 함수 포인터
static void BM_FunctionPointer(benchmark::State& state) {
int (*funcPtr)(int, int) = add;
for (auto _ : state) {
int result = funcPtr(10, 20);
benchmark::DoNotOptimize(result);
}
}
BENCHMARK(BM_FunctionPointer);
// std::function
static void BM_StdFunction(benchmark::State& state) {
std::function<int(int, int)> func = add;
for (auto _ : state) {
int result = func(10, 20);
benchmark::DoNotOptimize(result);
}
}
BENCHMARK(BM_StdFunction);
// 직접 호출 (기준)
static void BM_DirectCall(benchmark::State& state) {
for (auto _ : state) {
int result = add(10, 20);
benchmark::DoNotOptimize(result);
}
}
BENCHMARK(BM_DirectCall);
결과 (GCC 13, -O3):
BM_DirectCall 0.5 ns (인라인 최적화)
BM_FunctionPointer 1.0 ns (간접 호출)
BM_StdFunction 3.0 ns (타입 소거 + 간접 호출)
벤치마크: 람다 캡처
// 캡처 없는 람다 (함수 포인터)
static void BM_LambdaNoCapturePtr(benchmark::State& state) {
int (*funcPtr)(int, int) = { return a + b; };
for (auto _ : state) {
int result = funcPtr(10, 20);
benchmark::DoNotOptimize(result);
}
}
BENCHMARK(BM_LambdaNoCapturePtr);
// 캡처 없는 람다 (std::function)
static void BM_LambdaNoCaptureFunc(benchmark::State& state) {
std::function<int(int, int)> func = { return a + b; };
for (auto _ : state) {
int result = func(10, 20);
benchmark::DoNotOptimize(result);
}
}
BENCHMARK(BM_LambdaNoCaptureFunc);
// 캡처 있는 람다 (std::function만 가능)
static void BM_LambdaWithCapture(benchmark::State& state) {
int multiplier = 2;
std::function<int(int, int)> func = [multiplier](int a, int b) {
return (a + b) * multiplier;
};
for (auto _ : state) {
int result = func(10, 20);
benchmark::DoNotOptimize(result);
}
}
BENCHMARK(BM_LambdaWithCapture);
결과 (GCC 13, -O3):
BM_LambdaNoCapturePtr 1.0 ns
BM_LambdaNoCaptureFunc 3.0 ns
BM_LambdaWithCapture 3.5 ns (캡처 오버헤드 약간)
3. 사용 시나리오
함수 포인터 사용: 성능 중요
// ✅ 함수 포인터: 성능 중요한 콜백
class EventLoop {
using Callback = void (*)(int);
std::vector<Callback> callbacks_;
public:
void addCallback(Callback cb) {
callbacks_.push_back(cb);
}
void processEvents() {
for (int i = 0; i < 1000000; ++i) {
for (auto cb : callbacks_) {
cb(i); // 빠른 호출
}
}
}
};
void onEvent(int value) {
// 처리
}
int main() {
EventLoop loop;
loop.addCallback(onEvent);
loop.addCallback( { /* 처리 */ }); // 캡처 없는 람다
loop.processEvents(); // 빠름!
}
std::function 사용: 유연성 필요
// ✅ std::function: 유연한 콜백
class Button {
std::function<void()> onClick_;
public:
void setOnClick(std::function<void()> callback) {
onClick_ = callback;
}
void click() {
if (onClick_) {
onClick_();
}
}
};
int main() {
Button button;
// 람다 캡처
int clickCount = 0;
button.setOnClick([&clickCount]() {
++clickCount;
std::cout << "클릭 횟수: " << clickCount << '\n';
});
button.click(); // 클릭 횟수: 1
button.click(); // 클릭 횟수: 2
// 함수 객체
struct Logger {
void operator()() const {
std::cout << "버튼 클릭됨\n";
}
};
button.setOnClick(Logger{});
button.click(); // 버튼 클릭됨
}
std::function 사용: 멤버 함수
// ✅ std::function: 멤버 함수 바인딩
class Server {
public:
void handleRequest(const std::string& request) {
std::cout << "요청 처리: " << request << '\n';
}
};
class RequestHandler {
std::function<void(const std::string&)> handler_;
public:
void setHandler(std::function<void(const std::string&)> handler) {
handler_ = handler;
}
void process(const std::string& request) {
if (handler_) {
handler_(request);
}
}
};
int main() {
Server server;
RequestHandler handler;
// 멤버 함수 바인딩
handler.setHandler([&server](const std::string& req) {
server.handleRequest(req);
});
handler.process("GET /api/users"); // 요청 처리: GET /api/users
}
4. 실전 선택 가이드
함수 포인터 선택 기준
// ✅ 함수 포인터 사용 시나리오
// 1. 성능이 매우 중요
void processData(int* data, size_t size, int (*transform)(int)) {
for (size_t i = 0; i < size; ++i) {
data[i] = transform(data[i]); // 빠른 호출
}
}
// 2. C 라이브러리 인터페이스
extern "C" {
void register_callback(void (*callback)(int));
}
// 3. 단순한 콜백
void sort(int* arr, size_t size, bool (*compare)(int, int)) {
// 정렬
}
std::function 선택 기준
// ✅ std::function 사용 시나리오
// 1. 람다 캡처 필요
class Timer {
std::function<void()> callback_;
public:
void setCallback(std::function<void()> cb) {
callback_ = cb;
}
};
int timeout = 1000;
timer.setCallback([timeout]() {
std::cout << "타임아웃: " << timeout << "ms\n";
});
// 2. 다양한 호출 가능 객체
std::vector<std::function<void()>> tasks;
tasks.push_back( { /* 작업 1 */ });
tasks.push_back(MyFunctor{});
tasks.push_back([&]() { /* 작업 2 */ });
// 3. 타입 소거 필요
class EventDispatcher {
std::map<std::string, std::function<void(const Event&)>> handlers_;
public:
void on(const std::string& event, std::function<void(const Event&)> handler) {
handlers_[event] = handler;
}
};
메모리 오버헤드
함수 포인터: 8바이트
void (*funcPtr)(int) = nullptr;
std::cout << sizeof(funcPtr) << '\n'; // 8 (포인터 크기)
std::function: 32바이트 + 힙 할당
std::function<void(int)> func;
std::cout << sizeof(func) << '\n'; // 32 (구현마다 다름)
// 작은 캡처: 내부 버퍼 사용 (힙 할당 없음)
int x = 42;
func = [x](int y) { std::cout << x + y << '\n'; };
// 큰 캡처: 힙 할당
std::array<int, 100> bigData;
func = [bigData](int y) { /* ... */ }; // 힙 할당 발생
실전 예시
예시 1: 이벤트 시스템
// std::function: 유연한 이벤트 시스템
class EventSystem {
std::unordered_map<std::string, std::vector<std::function<void(const Event&)>>> listeners_;
public:
void addEventListener(const std::string& eventType,
std::function<void(const Event&)> listener) {
listeners_[eventType].push_back(listener);
}
void dispatchEvent(const std::string& eventType, const Event& event) {
auto it = listeners_.find(eventType);
if (it != listeners_.end()) {
for (auto& listener : it->second) {
listener(event);
}
}
}
};
int main() {
EventSystem events;
// 람다 캡처
int clickCount = 0;
events.addEventListener("click", [&clickCount](const Event& e) {
++clickCount;
std::cout << "클릭 " << clickCount << "회\n";
});
// 함수 객체
struct Logger {
void operator()(const Event& e) const {
std::cout << "이벤트 로그: " << e.type << '\n';
}
};
events.addEventListener("click", Logger{});
Event clickEvent{"click"};
events.dispatchEvent("click", clickEvent);
}
예시 2: 스레드 풀
// std::function: 작업 큐
class ThreadPool {
std::vector<std::thread> workers_;
std::queue<std::function<void()>> tasks_;
std::mutex mutex_;
std::condition_variable cv_;
bool stop_ = false;
public:
ThreadPool(size_t numThreads) {
for (size_t i = 0; i < numThreads; ++i) {
workers_.emplace_back([this]() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this]() { return stop_ || !tasks_.empty(); });
if (stop_ && tasks_.empty()) {
return;
}
task = std::move(tasks_.front());
tasks_.pop();
}
task(); // 작업 실행
}
});
}
}
template <typename F>
void enqueue(F&& task) {
{
std::unique_lock<std::mutex> lock(mutex_);
tasks_.emplace(std::forward<F>(task));
}
cv_.notify_one();
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(mutex_);
stop_ = true;
}
cv_.notify_all();
for (auto& worker : workers_) {
worker.join();
}
}
};
int main() {
ThreadPool pool(4);
// 다양한 작업 추가
pool.enqueue( { std::cout << "작업 1\n"; });
int x = 42;
pool.enqueue([x]() { std::cout << "작업 2: " << x << '\n'; });
pool.enqueue( {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "작업 3 완료\n";
});
}
정리
함수 포인터 vs std::function 선택
| 상황 | 사용 |
|---|---|
| 성능 중요 | 함수 포인터 |
| 람다 캡처 | std::function |
| 함수 객체 | std::function |
| 멤버 함수 | std::function |
| C 인터페이스 | 함수 포인터 |
| 타입 소거 | std::function |
| 단순 콜백 | 함수 포인터 |
핵심 규칙
- 성능 중요 → 함수 포인터
- 유연성 필요 → std::function
- 람다 캡처 → std::function
- 단순 콜백 → 함수 포인터
체크리스트
- 성능이 중요한가?
- 람다 캡처가 필요한가?
- 다양한 호출 가능 객체를 지원하는가?
- C 인터페이스와 호환되어야 하는가?
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 람다 기초 | Lambda 완벽 가이드
- C++ 콜백 패턴 | Callback 구현
- C++ 함수 객체 | Function Object
- C++ 성능 최적화 | 병목 찾기
이 글에서 다루는 키워드 (관련 검색어)
std::function, 함수 포인터, function vs 함수 포인터, 콜백, 람다 캡처 등으로 검색하시면 이 글이 도움이 됩니다.
실전 팁
실무에서 바로 적용할 수 있는 팁입니다.
디버깅 팁
- std::function은 nullptr 체크가 필요합니다
- 함수 포인터는 타입이 정확히 일치해야 합니다
- 람다 캡처 시 댕글링 참조를 주의하세요
성능 팁
- 성능이 중요하면 함수 포인터를 사용하세요
- std::function은 약 2~10배 느립니다
- 작은 캡처는 힙 할당이 없습니다
코드 리뷰 팁
- 성능이 중요한 곳의 std::function을 함수 포인터로 바꾸세요
- 람다 캡처가 필요하면 std::function을 사용하세요
- C 인터페이스는 함수 포인터를 사용하세요
자주 하는 실수
실수 1: 댕글링 참조 캡처
// ❌ 실수: 지역 변수 참조 캡처
std::function<int()> createFunction() {
int x = 42;
return [&x]() { return x; }; // ❌ x는 함수 종료 시 소멸
}
int main() {
auto func = createFunction();
std::cout << func() << '\n'; // ❌ 미정의 동작
}
// ✅ 값 캡처
std::function<int()> createFunction() {
int x = 42;
return [x]() { return x; }; // ✅ x를 복사
}
실수 2: 함수 포인터에 캡처 람다 할당
// ❌ 실수: 캡처 람다를 함수 포인터에
int multiplier = 2;
int (*func)(int) = [multiplier](int x) { return x * multiplier; };
// 컴파일 에러: cannot convert lambda with captures to function pointer
// ✅ std::function 사용
std::function<int(int)> func = [multiplier](int x) { return x * multiplier; };
실수 3: nullptr 체크 누락
// ❌ 실수: nullptr 체크 없음
std::function<void()> callback;
callback(); // ❌ std::bad_function_call 예외
// ✅ nullptr 체크
std::function<void()> callback;
if (callback) {
callback();
}
실무 트러블슈팅
문제: 성능 저하
증상:
// 콜백이 많이 호출되는데 느림
for (int i = 0; i < 1000000; ++i) {
callback(i); // std::function 사용
}
진단:
// 프로파일링
// std::function: 3초
// 함수 포인터: 1초
// 직접 호출: 0.5초
해결:
// 1. 함수 포인터로 변경
void (*callback)(int) = myFunc;
// 2. 인라인 함수 사용
inline void callback(int x) { /* ... */ }
// 3. 템플릿으로 정적 디스패치
template <typename Func>
void process(Func callback) {
for (int i = 0; i < 1000000; ++i) {
callback(i);
}
}
문제: 메모리 누수
증상:
class Widget {
std::function<void()> onClick_;
public:
void setOnClick(std::function<void()> callback) {
onClick_ = callback;
}
};
// 순환 참조
auto widget = std::make_shared<Widget>();
widget->setOnClick([widget]() { // ❌ 순환 참조
widget->doSomething();
});
해결:
// weak_ptr 사용
auto widget = std::make_shared<Widget>();
widget->setOnClick([weak = std::weak_ptr<Widget>(widget)]() {
if (auto ptr = weak.lock()) {
ptr->doSomething();
}
});
성능 분석 상세
호출 오버헤드 분해
// 1. 직접 호출
int add(int a, int b) { return a + b; }
int result = add(1, 2); // 0.5ns (인라인 가능)
// 2. 함수 포인터
int (*funcPtr)(int, int) = add;
int result = funcPtr(1, 2); // 1.0ns (간접 호출)
// 3. std::function
std::function<int(int, int)> func = add;
int result = func(1, 2); // 3.0ns (타입 소거 + 간접 호출)
메모리 오버헤드
| 타입 | 크기 | 힙 할당 | 비고 |
|---|---|---|---|
| 함수 포인터 | 8B | 없음 | 포인터만 |
| std::function (작은 캡처) | 32B | 없음 | SBO 적용 |
| std::function (큰 캡처) | 32B | 있음 | 힙 할당 |
// SBO (Small Buffer Optimization)
int x = 42;
std::function<int()> func1 = [x]() { return x; }; // SBO (힙 할당 없음)
std::array<int, 100> bigData;
std::function<int()> func2 = [bigData]() { return 0; }; // 힙 할당
베스트 프랙티스
1. 콜백 인터페이스 설계
// ✅ 성능 중요: 함수 포인터
class HighPerformanceTimer {
using Callback = void (*)(void* userData);
Callback callback_;
void* userData_;
public:
void setCallback(Callback cb, void* data) {
callback_ = cb;
userData_ = data;
}
void tick() {
if (callback_) {
callback_(userData_); // 빠른 호출
}
}
};
// ✅ 유연성 중요: std::function
class EventEmitter {
std::unordered_map<std::string, std::vector<std::function<void(const Event&)>>> listeners_;
public:
void on(const std::string& event, std::function<void(const Event&)> listener) {
listeners_[event].push_back(listener);
}
};
2. 템플릿으로 제로 오버헤드
// ✅ 템플릿: 정적 디스패치
template <typename Callback>
class Timer {
Callback callback_;
public:
Timer(Callback cb) : callback_(cb) {}
void tick() {
callback_(); // 인라인 가능
}
};
// 사용
Timer timer( { std::cout << "Tick\n"; });
// 컴파일러가 람다를 인라인 전개 가능
3. 코드 리뷰 체크포인트
// 🔍 리뷰 시 확인사항
// 1. 성능 중요한 루프
for (int i = 0; i < 1000000; ++i) {
callback(i); // ⚠️ std::function? 함수 포인터?
}
// 2. 람다 캡처
[&]() { /* ... */ } // ⚠️ 댕글링 참조 가능성?
// 3. 순환 참조
[shared_ptr]() { /* ... */ } // ⚠️ 순환 참조?
// 4. nullptr 체크
callback(); // ⚠️ nullptr 체크?
실무 시나리오
시나리오 1: 게임 이벤트 시스템
// ✅ 실무 예시: 게임 이벤트
class GameEventSystem {
// 성능 중요: 프레임마다 호출
using FastCallback = void (*)(int entityId);
std::vector<FastCallback> updateCallbacks_;
// 유연성 중요: 가끔 호출
std::unordered_map<std::string, std::vector<std::function<void(const Event&)>>> eventHandlers_;
public:
// 빠른 업데이트 콜백
void registerUpdate(FastCallback callback) {
updateCallbacks_.push_back(callback);
}
void update() {
// 매 프레임 호출 - 빠름
for (auto callback : updateCallbacks_) {
callback(0);
}
}
// 이벤트 핸들러
void on(const std::string& event, std::function<void(const Event&)> handler) {
eventHandlers_[event].push_back(handler);
}
void emit(const std::string& event, const Event& e) {
// 가끔 호출 - 유연성
auto it = eventHandlers_.find(event);
if (it != eventHandlers_.end()) {
for (auto& handler : it->second) {
handler(e);
}
}
}
};
시나리오 2: HTTP 서버
// ✅ 실무 예시: HTTP 라우터
class HttpServer {
using Handler = std::function<void(const Request&, Response&)>;
std::unordered_map<std::string, Handler> routes_;
public:
void get(const std::string& path, Handler handler) {
routes_["GET:" + path] = handler;
}
void post(const std::string& path, Handler handler) {
routes_["POST:" + path] = handler;
}
void handleRequest(const Request& req, Response& res) {
std::string key = req.method + ":" + req.path;
auto it = routes_.find(key);
if (it != routes_.end()) {
it->second(req, res);
} else {
res.status(404).send("Not Found");
}
}
};
// 사용
HttpServer server;
// 람다 캡처 활용
Database db;
server.get("/users", [&db](const Request& req, Response& res) {
auto users = db.query("SELECT * FROM users");
res.json(users);
});
server.post("/users", [&db](const Request& req, Response& res) {
auto user = req.body<User>();
db.insert(user);
res.status(201).send("Created");
});
시나리오 3: 비동기 작업 큐
// ✅ 실무 예시: 작업 큐
class TaskQueue {
std::queue<std::function<void()>> tasks_;
std::mutex mutex_;
std::condition_variable cv_;
std::vector<std::thread> workers_;
bool stop_ = false;
public:
TaskQueue(size_t numWorkers) {
for (size_t i = 0; i < numWorkers; ++i) {
workers_.emplace_back([this]() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this]() { return stop_ || !tasks_.empty(); });
if (stop_ && tasks_.empty()) {
return;
}
task = std::move(tasks_.front());
tasks_.pop();
}
task(); // 작업 실행
}
});
}
}
template <typename F>
void enqueue(F&& task) {
{
std::unique_lock<std::mutex> lock(mutex_);
tasks_.emplace(std::forward<F>(task));
}
cv_.notify_one();
}
~TaskQueue() {
{
std::unique_lock<std::mutex> lock(mutex_);
stop_ = true;
}
cv_.notify_all();
for (auto& worker : workers_) {
worker.join();
}
}
};
// 사용
TaskQueue queue(4);
// 다양한 작업 추가
queue.enqueue( { std::cout << "작업 1\n"; });
int x = 42;
queue.enqueue([x]() { std::cout << "작업 2: " << x << '\n'; });
queue.enqueue([&db = database]() {
db.cleanup();
});
최신 C++ 대안
C++20: Concepts로 제약
// C++20: Callable concept
template <typename F>
concept Callback = std::invocable<F, int>;
template <Callback F>
void process(F callback) {
callback(42);
}
// 컴파일 타임에 타입 체크
process( { std::cout << x << '\n'; }); // OK
// process( { /* ... */ }); // 컴파일 에러
마치며
std::function은 유연하지만 느리고, 함수 포인터는 빠르지만 제한적입니다.
핵심 원칙:
- 성능 중요 → 함수 포인터
- 유연성 필요 → std::function
- 람다 캡처 → std::function
실무 팁:
- 프레임마다 호출되는 콜백은 함수 포인터
- 이벤트 핸들러는 std::function
- 템플릿으로 제로 오버헤드 추구
성능과 유연성을 고려해 적절히 선택하세요.
다음 단계: std::function을 이해했다면, C++ 람다 가이드에서 더 깊이 배워보세요.
관련 글
- C++ 시리즈 전체 보기
- C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
- C++ ADL |
- C++ Aggregate Initialization |