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

[C++] Initializer Lists / Uniform Initialization / Designated Initializers

by 별준 2022. 2. 9.

References

Contents

  • Initializer Lists
  • Uniform Initialization
  • Designated Initializers

최근 모던 C++ 문법에 대해 알아보면서, 예제 코드에서 익숙하지 않은 변수 초기화 등을 사용했는데, 한 번 정리할 필요가 있어서 이번 포스팅을 준비했습니다. 아마 대부분의 초기화 방법은 익숙할텐데, 저의 경우에는 자주 접하지 않은 Initializer list와 uniform initialization, 그리고 C++20부터 도입된 designated initializers에 대해 알아보도록 하겠습니다.

 


Initializer Lists (이니셜라이저 리스트)

(since C++11) Initializer lists는 <initializer_list>에 정의되어 있으며, 이를 활용하면 여러 인수를 받는 함수를 쉽게 작성할 수 있습니다. std::initializer_list 의 타입은 클래스 템플릿입니다. 그래서 vector에 저장할 객체의 타입을 지정할 때처럼 원소 타입을 angle brackets(<>)에 지정해주어야 합니다. 다음 예제를 통해 구체적인 방법을 살펴보겠습니다.

#include <initializer_list>

int makeSum(std::initializer_list<int> lst)
{
    int total = 0;
    for (int value : lst) {
        total += value;
    }

    return total;
}

여기서 makeSum() 함수는 정수에 대한 initializer list를 인수로 받습니다. 그리고 함수의 바디에서 범위 기반(range-based) for 루프를 사용하여 인수로 주어진 정수들을 모두 더합니다. 이 함수를 호출하는 방법은 다음과 같습니다.

int a = makeSum({1, 2, 3});
int b = makeSum({10, 20, 30, 40, 50, 60});

intializer list는 type safe 합니다. 따라서 리스트의 모든 원소는 반드시 동일한 타입이어야합니다. 위에서 보듯이 makeSum() 함수로 전달되는 initializer list의 모든 원소는 정수형입니다. 만약 아래 코드처럼 정수값 중의 하나를 double로 변경하면 컴파일 에러가 발생합니다.

int c = makeSum({ 1, 2, 3.0});

 


Uniform Initialization

C++11 이전에는 타입에 대한 초기화가 일정(uniform)하지 않았습니다.

예를 들어, 아래의 circle에 대한 클래스와 구조체 정의를 살펴보겠습니다.

struct CircleStruct
{
    int x, y;
    double radius;
};

class CircleClass
{
public:
    CircleClass(int x, int y, double radius)
        : m_x(x), m_y(y), m_radius(radius) {}
private:
    int m_x, m_y;
    double m_radius;
};

C++11 이전에는 CircleStruct 타입의 변수와 CircleClass 타입의 변수를 초기화하는 방법이 달랐습니다.

CircleStruct myCircle1 = { 10, 10, 2.5 }:
CircleClass myCircle2(10, 10, 2.5);

구조체에 대해서는 {...} 문법을 사용했지만, 클래스에는 함수 표기법인 (...)로 생성자를 호출했습니다.

 

하지만, C++11부터는 타입을 초기화할 때 {...} 문법을 사용하는 uniform initialization을 따르도록 통일되었습니다.

uniform initialization은 유니폼 초기화, 균일 초기화, 중괄호 초기화라고 불립니다.

CircleStruct myCircle3 = { 10, 10, 2.5 };
CircleClass myCircle4 = { 10, 10, 2.5 };

myCircle4를 정의하는 문장이 실행될 때, CircleClass의 생성자가 자동으로 호출됩니다. 또한, 아래와 같이 등호(=)를 생략해도 됩니다.

CircleStruct myCircle5{ 10, 10, 2.5 };
CircleClass myCircle6{ 10, 10, 2.5 };

 

또 다른 예제로, Employee 구조체를 살펴보겠습니다.

struct Employee {
    char firstInitial;
    char lastInitial;
    int employeeNumber;
    int salary;
};

이 구조체의 초기화는 보통 다음과 같이 할 수 있습니다.

Employee anEmployee;
anEmployee.firstInitial = 'J';
anEmployee.lastInitial = 'D';
anEmployee.employeeNumber = 42;
anEmployee.salary = 80'000;

하지만 uniform initialization을 사용하면, 다음과 같이 간단하게 초기화할 수 있습니다.

Employee anEmployee{ 'J', 'D', 42, 80'000 };

 

uniform initialization은 구조체와 클래스에만 국한되는 것이 아니며, C++에 있는 모든 대상을 초기화하는데 적용됩니다. 예를 들어, 다음 4개의 변수에 대한 초기화는 모두 3이라는 값으로 초기화합니다.

int a = 3;
int b(3);
int c = { 3 };  // uniform initialization
int d{ 3 };     // uniform initialization

 

uniform initialization는 변수의 zero-initialization을 수행하는데 사용할 수 있습니다. 다음과 같이 빈 중괄호를 사용해주면 됩니다.

int e{};

 

uniform initialization을 사용하면 narrowing(축소 변환; narrowing conversion)을 방지할 수 있습니다. 예전 스타일의 할당 문법을 사용하여 변수를 초기화하면, C++은 암시적으로 narrowing을 수행합니다.

void func(int i) { /* ... */ }

int main()
{
    int x = 3.14;
    func(3.14);
}

x 변수에 값을 대입할 때와 func() 함수를 호출할 때 전달한 3.14는 자동으로 3으로 잘립니다. 컴파일러에 따라서 경고 메세지가 발생할 수도 있습니다. 하지만 uniform initialization을 사용하면 x에 값을 대입하거나 func()를 호출하는 문장이 담긴 코드를 C++11 표준을 완전히 지원하는 컴파일러로 컴파일하면 반드시 에러 메세지가 발생합니다.

int x{ 3.14 };  // error becuase narrowing
func({ 3.14 }); // error because narrowing

만약 narrowing cast가 필요하다면, gls::narrow_cast() 함수(in Guildline Support Library(GSL))를 사용하는 것을 권장합니다.

 

uniform initialization은 동적으로 할당되는 배열을 초기화할 때도 적용할 수 있습니다. 예를 들면, 다음과 같습니다.

int* myArray = new int[4]{ 0, 1, 2, 3 };

C++20부터는 배열의 크기를 생략할 수도 있습니다.

int* myArray = new int[]{ 0, 1, 2, 3 };

 

또한, 클래스 멤버인 배열을 생성자 이니셜라이저로 초기화할 때도 사용할 수 있습니다.

class MyClass
{
public:
    MyClass() : mArray{ 0, 1, 2, 3} {}

private:
    int mArray[4];
};

uniform initialization은 std::vector와 같은 표준 라이브러리 컨테이너에도 적용할 수 있습니다.

 

uniform initialization에 관련하여 아래 글도 잘 설명해주고 있으니, 참고하셔도 좋을 것 같습니다 !

https://modoocode.com/286

 

씹어먹는 C++ - <16 - 1. C++ 유니폼 초기화(Uniform Initialization)>

 

modoocode.com


Designated Initializers (link)

C++20부터는 designated initializers(지정된 초기화)가 도입되었습니다. 이를 사용하여 소위 aggregates라고 불리는 데이터 멤버를 이 데이터 멤버 이름으로 초기화할 수 있습니다. aggregate type(집합 타입)은 다음의 제약을 만족하는 배열 타입의 객체, 또는 구조체나 클래스의 객체입니다.

  • only public 데이터 멤버
  • no user-declared or inherited constructor(생성자)
  • no virtual function
  • no virtual, private, or protected base classes

 

designated initializer는 점(.)으로 시작하며 다음에 데이터 이름이 나옵니다. designated initializer는 반드시 데이터 멤버가 선언된 순서와 동일한 순서이어야 합니다. designated initializer와 non-designated initializer를 혼합하여 사용하는 것은 허용되지 않습니다. designated initializer를 사용하여 초기화되지 않은 데이터 멤버는 그들의 기본값으로 초기화되며, 다음과 같이 초기화됩니다.

  • in-class 이니셜라이저를 가지고 있는 데이터 멤버는 그 값으로 초기화됨
  • in-class 이니셜라이저가 없다면 0으로 초기화됨

 

다음의 Employee 구조체를 살펴보겠습니다. salary 데이터 멤버는 기본값이 75,000으로 설정되어 있습니다.

struct Employee {
    char firstInitial;
    char lastInitial;
    int employeeNumber;
    int salary{ 75'000 };
};

위에서 이 구조체는 다음과 같이 uniform initialization 문법을 사용하여 초기화할 수 있었습니다.

Employee anEmployee{ 'J', 'D', 42, 80'000 };

 

designated initializer를 사용하면 다음과 같이 초기화할 수 있습니다.

Employee anEmployee {
    .firstInitial = 'J',
    .lastInitial = 'D',
    .employeeNumber = 42,
    .salary = 80'000
};

uniform initialization 문법과 비교해서 designated initializer를 사용하여 얻을 수 있는 장점은 더 쉽게 어떤 멤버들이 무슨 값으로 초기화되는지 이해할 수 있습니다.

 

designated initializer를 사용할 때, 기본값으로 설정되어도 상관없다면 특정 데이터 멤버의 초기화를 스킵할 수 있습니다. 예를 들어, 아래 코드는 employee를 생성할 때, employeeNumber를 생략했으며, 이 멤버는 in-class 이니셜라이저가 없기 때문에 0으로 초기화됩니다.

Employee anEmployee {
    .firstInitial = 'J',
    .lastInitial = 'D',
    .salary = 80'000
};

uniform initialization을 사용하면, 위와 같이 중간 멤버를 생략하는 것이 불가능하며, 명시적으로 0으로 지정해주어야 합니다.

Employee anEmployee{ 'J', 'D', 0, 80'000 };

 

만약 다음과 같이 salary 데이터 멤버의 초기화를 생략하면, salary는 in-class 이니셜라이저 값인 75,000으로 초기화됩니다.

Employee anEmployee {
    .firstInitial = 'J',
    .lastInitial = 'D'
};

 

designated initializer를 사용해서 얻을 수 있는 또 한 가지 장점은 새로운 멤버가 데이터 구조체에 추가될 때, designated initializer를 사용하던 기존 코드를 수정하지 않아도 정상적으로 동작된다는 것입니다. 새로 추가되는 데이터 멤버는 그들의 기본값으로 초기화될 것 입니다.

댓글