본문 바로가기
개발/C++ (98,03,11,14,17,20,23)

Modern C++ : std::initializer_list

by snowoods 2025. 11. 27.

Modern C++

std::initializer_list

 

개요

std::initializer_list는 C++11에서 도입된 템플릿 클래스로, 중괄호 초기화 리스트(braced-init-list)를 표현하는 데 사용됩니다. 이를 통해 함수에서 배열과 유사한 구문으로 여러 값을 전달할 수 있어 코드를 더 직관적이고 간결하게 만들 수 있습니다.

 

C++ 버전별 도입 시기

  • C++11: std::initializer_list 도입
  • C++14: 개선된 기능들
  • C++17: 구조적 바인딩과의 호환성 개선
  • C++20: 더 나은 추론 지원

 

std::initializer_list의 특징

기본 구조

template<class T>
class initializer_list {
public:
    using value_type = T;
    using reference = const T&;
    using const_reference = const T&;
    using size_type = size_t;

    size_type size() const noexcept;
    const T* begin() const noexcept;
    const T* end() const noexcept;
};

 

주요 특징

  • 읽기 전용: 요소를 수정할 수 없습니다
  • 가벼운 래퍼: 실제 데이터는 스택에 저장됩니다
  • 범위 기반 for 루프 지원: 쉽게 순회할 수 있습니다

사용 방법

1. 함수 파라미터로 사용

#include <initializer_list>
#include <vector>
#include <iostream>

void printNumbers(std::initializer_list<int> numbers) {
    for (int num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
}

int main() {
    printNumbers({1, 2, 3, 4, 5});
    printNumbers({10, 20, 30});
    return 0;
}

 

2. 생성자에서 사용

class MyContainer {
private:
    std::vector<int> data;

public:
    MyContainer(std::initializer_list<int> init) 
        : data(init.begin(), init.end()) {}

    void print() const {
        for (int val : data) {
            std::cout << val << " ";
        }
        std::cout << std::endl;
    }
};

int main() {
    MyContainer container{1, 2, 3, 4, 5};
    container.print();
    return 0;
}

 

3. STL 컨테이너 초기화

#include <vector>
#include <map>
#include <string>

int main() {
    // 벡터 초기화
    std::vector<int> vec{1, 2, 3, 4, 5};

    // 맵 초기화
    std::map<std::string, int> scores{
        {"Alice", 95},
        {"Bob", 87},
        {"Charlie", 92}
    };

    return 0;
}

 

std::initializer_list vs std::vector

std::initializer_list의 장점

  • 간결한 문법: {}를 사용한 직관적인 초기화
  • 컴파일 타임 최적화: 불필요한 복사가 없음
  • 가벼움: 오버헤드가 최소화됨

std::vector의 장점

  • 동적 크기: 런타임에 크기 변경 가능
  • 수정 가능: 요소 추가, 삭제, 수정 가능
  • 풍부한 API: 다양한 멤버 함수 제공

선택 가이드

// initializer_list가 적합한 경우
class Point {
public:
    Point(std::initializer_list<double> coords) {
        // 좌표 초기화 (읽기 전용)
    }
};

// vector가 적합한 경우
class DataProcessor {
public:
    void processData(std::vector<int> data) {
        // 데이터 수정 및 처리
        data.push_back(42);
    }
};

 

템플릿과 함께 사용

템플릿 함수

template<typename T>
T sum(std::initializer_list<T> values) {
    T total = T{};
    for (const T& value : values) {
        total += value;
    }
    return total;
}

int main() {
    int result1 = sum({1, 2, 3, 4, 5});        // 15
    double result2 = sum({1.1, 2.2, 3.3});      // 6.6
    return 0;
}

 

가변 템플릿과의 비교

// initializer_list 사용
void printValues(std::initializer_list<int> values) {
    for (int val : values) {
        std::cout << val << " ";
    }
}

// 가변 템플릿 사용
template<typename... Args>
void printValues(Args... args) {
    ((std::cout << args << " "), ...);
}

int main() {
    printValues({1, 2, 3, 4, 5});  // initializer_list
    printValues(1, 2, 3, 4, 5);    // 가변 템플릿
    return 0;
}

 

한계와 주의사항

1. empty() 메서드 없음

void processList(std::initializer_list<int> list) {
    // 틀림: list.empty()
    // 맞음: list.size() == 0
    if (list.size() == 0) {
        std::cout << "Empty list" << std::endl;
    }
}

 

2. std::make_unique와의 호환성 문제

class MyClass {
public:
    MyClass(std::initializer_list<int> list) {}
};

// 템플릿 타입 추론 실패
// auto obj = std::make_unique<MyClass>({1, 2, 3});

// 해결책 1: 명시적 타입 지정
auto obj1 = std::make_unique<MyClass>(std::initializer_list<int>{1, 2, 3});

// 해결책 2: 직접 new 사용
auto obj2 = std::unique_ptr<MyClass>(new MyClass{1, 2, 3});

// 해결책 3: std::vector 사용
class MyClassVector {
public:
    MyClassVector(const std::vector<int>& vec) {}
};
auto obj3 = std::make_unique<MyClassVector>(std::vector<int>{1, 2, 3});

 

3. 수명 주의

std::initializer_list<int> getBadList() {
    return {1, 2, 3};  // 미정의 동작! 임시 객체의 수명
}

std::vector<int> getGoodList() {
    return {1, 2, 3};  // 안전: vector로 복사됨
}

 

실용적인 예제

1. 수학 함수들

class MathUtils {
public:
    static double average(std::initializer_list<double> values) {
        if (values.size() == 0) return 0.0;

        double sum = 0.0;
        for (double val : values) {
            sum += val;
        }
        return sum / values.size();
    }

    static double max(std::initializer_list<double> values) {
        if (values.size() == 0) throw std::invalid_argument("Empty list");

        double maxVal = *values.begin();
        for (double val : values) {
            if (val > maxVal) maxVal = val;
        }
        return maxVal;
    }
};

 

2. 설정 클래스

class Config {
private:
    std::map<std::string, std::string> settings;

public:
    Config(std::initializer_list<std::pair<std::string, std::string>> init) {
        for (const auto& pair : init) {
            settings[pair.first] = pair.second;
        }
    }

    std::string get(const std::string& key) const {
        auto it = settings.find(key);
        return it != settings.end() ? it->second : "";
    }
};

int main() {
    Config config{
        {"host", "localhost"},
        {"port", "8080"},
        {"debug", "true"}
    };

    std::cout << "Host: " << config.get("host") << std::endl;
    return 0;
}

 

3. 그래픽스 객체

class Color {
private:
    float r, g, b, a;

public:
    Color(std::initializer_list<float> components) {
        auto it = components.begin();
        r = (it != components.end()) ? *it++ : 0.0f;
        g = (it != components.end()) ? *it++ : 0.0f;
        b = (it != components.end()) ? *it++ : 0.0f;
        a = (it != components.end()) ? *it++ : 1.0f;
    }

    void print() const {
        std::cout << "RGBA(" << r << ", " << g << ", " << b << ", " << a << ")" << std::endl;
    }
};

int main() {
    Color red{1.0f, 0.0f, 0.0f};
    Color semiTransparentBlue{0.0f, 0.0f, 1.0f, 0.5f};

    red.print();
    semiTransparentBlue.print();
    return 0;
}

 

성능 고려사항

컴파일 타임 최적화

// 컴파일러가 최적화할 수 있는 경우
void processStatic() {
    static const std::initializer_list<int> data{1, 2, 3, 4, 5};
    // 컴파일 타임에 데이터가 상수로 처리됨
}

// 런타임 처리가 필요한 경우
void processDynamic() {
    std::vector<int> data{1, 2, 3, 4, 5};
    // 동적 메모리 할당 및 복사 발생
}

 

메모리 사용량

  • std::initializer_list: 포인터 + 크기 (보통 16바이트)
  • std::vector: 포인터 + 크기 + 용량 + 동적 할당

모범 사례

1. 적절한 사용 시나리오

  • 생성자 초기화: 객체 생성 시 여러 값 전달
  • 설정 파라미터: 읽기 전용 설정 값 전달
  • 수학적 연산: 여러 숫자를 입력으로 받는 함수

2. 피해야 할 경우

  • 데이터 수정 필요: std::vector 사용
  • 동적 크기 변경: std::vector 사용
  • 복잡한 템플릿 추론: 명시적 타입 지정이 필요한 경우

3. 코드 스타일

// 좋은 예시
class Point {
public:
    Point(std::initializer_list<double> coords) : 
        x(coords.size() > 0 ? *coords.begin() : 0.0),
        y(coords.size() > 1 ? *(coords.begin() + 1) : 0.0) {}
private:
    double x, y;
};

// 나쁜 예시
class BadPoint {
public:
    BadPoint(std::initializer_list<double> coords) {
        // 경계 검사 없이 접근 - 위험
        x = *coords.begin();
        y = *(coords.begin() + 1);
    }
private:
    double x, y;
};

 

결론

std::initializer_list는 C++에서 중괄호 초기화 문법을 활용하여 코드를 더 직관적이고 간결하게 만드는 강력한 도구입니다. 특히 생성자와 설정 함수에서 여러 값을 전달할 때 유용하지만, 템플릿 타입 추론의 한계와 수정 불가능한 특성을 이해하고 적절한 상황에서 사용하는 것이 중요합니다.

std::make_unique와의 호환성 문제와 같은 실용적인 제한들을 인지하고, 필요할 때는 std::vector와 같은 대안을 고려하는 것이 좋은 프로그래밍 습관입니다.