C ++ 11 Uniform Initialization은 이전 스타일 구문을 대체합니까?


172

C ++ 11의 균일 한 초기화는 언어의 구문 상 모호성을 해결하지만 많은 Bjarne Stroustrup의 프레젠테이션 (특히 GoingNative 2012 대화 중 프레젠테이션)에서 그의 예제는 주로 객체를 생성 할 때 마다이 구문을 사용한다는 것을 이해합니다.

모든 경우에 균일 한 초기화를 사용하는 것이 권장 됩니까? 코딩 스타일과 일반적인 사용법에 관한 한이 새로운 기능에 대한 일반적인 접근 방식은 무엇입니까? 사용 하지 않는 이유는 무엇입니까 ?

내 마음에 나는 주로 객체 구성을 나의 유스 케이스로 생각하고 있지만 다른 시나리오가 있다면 알려 주시기 바랍니다.


이것은 Programmers.se에서 더 잘 논의되는 주제 일 수 있습니다. 좋은 주관적인쪽에 기대는 것 같습니다.
Nicol Bolas

6
@NicolBolas : 반면에, 귀하의 탁월한 답변은 c ++-faq 태그에 대한 훌륭한 후보가 될 수 있습니다. 이전에 게시 된 내용에 대해 설명하지 않았다고 생각합니다.
Matthieu M.

답변:


233

코딩 스타일은 궁극적으로 주관적이며 상당한 성능상의 이점이 없을 것입니다. 그러나 여기에 균일 한 초기화를 자유로이 사용하면 얻을 수 있다고 말할 것입니다.

중복 유형 이름 최소화

다음을 고려하세요:

vec3 GetValue()
{
  return vec3(x, y, z);
}

vec3두 번 입력해야 합니까? 그 점이 있습니까? 컴파일러는 함수가 무엇을 반환하는지 잘 알고 있습니다. 왜 "이 값으로 반환하는 것을 생성자에게 호출하여 반환합니까?" 균일 한 초기화를 통해 다음을 수행 할 수 있습니다.

vec3 GetValue()
{
  return {x, y, z};
}

모든 것이 작동합니다.

함수 인수가 더 좋습니다. 이걸 고려하세요:

void DoSomething(const std::string &str);

DoSomething("A string.");

암시 적으로 std::string자체를 빌드하는 방법을 알고 있기 때문에 유형 이름을 입력하지 않아도 작동합니다 const char*. 훌륭합니다. 그러나 RapidXML에 따르면 해당 문자열이 나온다면 어떨까요? 또는 루아 문자열. 즉, 실제로 문자열의 길이를 미리 알고 있다고 가정 해 봅시다. 를 std::string생성하는 생성자는 const char*그냥 a를 전달하면 문자열의 길이를 가져와야합니다 const char*.

그래도 명시 적으로 길이가 걸리는 과부하가 있습니다. 그러나 그것을 사용하려면 다음을 수행해야합니다 DoSomething(std::string(strValue, strLen)). 왜 여분의 typename이 있습니까? 컴파일러는 유형이 무엇인지 알고 있습니다. 와 마찬가지로 auto추가 유형 이름을 피할 수 있습니다.

DoSomething({strValue, strLen});

그냥 작동합니다. 타입 이름도없고, 소란도없고, 아무것도 없습니다. 컴파일러는 작업을 수행하고 코드는 더 짧으며 모두가 행복합니다.

물론 첫 번째 버전 ( DoSomething(std::string(strValue, strLen)))이 더 읽기 쉽다 는 주장이 있습니다 . 즉, 무슨 일이 일어나고 있고 누가 무엇을하고 있는지가 분명합니다. 어느 정도는 사실이다. 균일 한 초기화 기반 코드를 이해하려면 함수 프로토 타입을 살펴 봐야합니다. 이것이 어떤 사람들이 비 const 참조로 매개 변수를 전달해서는 안되는 것과 같은 이유입니다. 따라서 값이 수정되는 경우 콜 사이트에서 볼 수 있습니다.

그러나 똑같이 말할 수있다 auto. 당신이 얻는 것을 알기 auto v = GetSomething();위해서는의 정의를 살펴 봐야합니다 GetSomething. 그러나 auto일단 당신이 그것에 접근하면 거의 무모한 포기와 함께 사용되는 것을 멈추지 않았습니다 . 개인적으로 일단 익숙해지면 괜찮을 것 같습니다. 특히 좋은 IDE.

가장 Vexing 구문 분석을하지 마십시오

코드는 다음과 같습니다.

class Bar;

void Func()
{
  int foo(Bar());
}

팝 퀴즈 : 무엇 foo입니까? "변수"라고 대답하면 틀린 것입니다. 실제로 매개 변수로 사용을 반환하는 함수를 취하는 함수의 프로토 타입입니다 Bar, 그리고 foo함수의 반환 값은 int 형이다.

이것을 C ++의 "가장 Vexing 구문 분석"이라고합니다. 인간에게는 전혀 의미가 없기 때문입니다. 그러나 C ++의 규칙은 슬프게도 이것을 요구합니다. 함수 프로토 타입으로 해석 될 수 있다면 그렇게 될 입니다. 문제는 Bar(); 그것은 두 가지 중 하나 일 수 있습니다. 이라는 유형이 될 수 있으며 Bar이는 임시를 작성 중임을 의미합니다. 또는 매개 변수를 사용하지 않고 a를 반환하는 함수일 수 있습니다 Bar.

균일 한 초기화는 함수 프로토 타입으로 해석 될 수 없습니다.

class Bar;

void Func()
{
  int foo{Bar{}};
}

Bar{}항상 임시를 만듭니다. int foo{...}항상 변수를 만듭니다.

사용하려는 경우가 많지만 Typename()C ++의 구문 분석 규칙으로 인해 불가능한 경우가 있습니다 . 로 Typename{}, 모호함이 없습니다.

하지 말아야 할 이유

포기한 유일한 힘은 좁아지는 것입니다. 균일 한 초기화로 큰 값으로 작은 값을 초기화 할 수 없습니다.

int val{5.2};

컴파일되지 않습니다. 구식 초기화로 할 수 있지만 균일 한 초기화는 불가능합니다.

이니셜 라이저 목록을 실제로 작동시키기 위해 부분적으로 수행되었습니다. 그렇지 않으면 이니셜 라이저 목록의 유형과 관련하여 많은 모호한 경우가 있습니다.

물론, 어떤 사람들은 그러한 코드 컴파일 할 자격 이 없다고 주장 할 수도 있습니다 . 나는 개인적으로 동의합니다. 좁히는 것은 매우 위험하며 불쾌한 행동을 유발할 수 있습니다. 컴파일러 단계에서 이러한 문제를 조기에 파악하는 것이 가장 좋습니다. 최소한 좁히는 것은 누군가 코드에 대해 너무 열심히 생각하지 않는다는 것을 암시합니다.

경고 수준이 높으면 컴파일러에서 일반적으로 이러한 종류의 경고를 표시합니다. 실제로이 모든 것은 경고를 강제 오류로 만드는 것입니다. 어떤 사람들은 당신이 어쨌든 그렇게해야한다고 말할 수도 있습니다.)

하지 말아야 할 또 다른 이유가 있습니다.

std::vector<int> v{100};

이것은 무엇을 하는가? vector<int>기본 구성 항목 100 개로을 만들 수 있습니다 . 또는 vector<int>값이 인 1 개의 항목으로를 만들 수 있습니다 100. 둘 다 이론적으로 가능합니다.

실제로는 후자를 수행합니다.

왜? 이니셜 라이저 목록은 균일 한 초기화와 동일한 구문을 사용합니다. 모호한 경우 어떻게해야하는지 설명하는 규칙이 필요합니다. 규칙은 매우 간단합니다. 컴파일러 중괄호 초기화 목록과 함께 초기화 목록 생성자를 사용할 있으면 됩니다 . 이후 vector<int>취하는 초기화 목록 생성자가 initializer_list<int>, 그리고 {100} 유효한 수를 initializer_list<int>, 따라서 해야합니다 .

사이징 생성자를 얻으려면 ()대신을 사용해야 합니다 {}.

이것이 vector정수로 변환 할 수없는 것이면 이런 일이 발생하지 않습니다. initializer_list는 해당 vector유형 의 초기화 목록 생성자에 맞지 않으므로 컴파일러는 다른 생성자에서 자유롭게 선택할 수 있습니다.


11
+1 못 박았다. 귀하의 답변이 동일한 지점을 훨씬 더 자세히 다루기 때문에 답변을 삭제하고 있습니다.
R. Martinho Fernandes 2012

21
마지막 요점은 내가 정말로 좋아하는 이유 std::vector<int> v{100, std::reserve_tag};입니다. 와 마찬가지로 std::resize_tag. 현재 벡터 공간을 예약하려면 두 단계가 필요합니다.
Xeo

6
@NicolBolas-두 가지 점 : vexing 구문 분석의 문제는 Bar ()가 아니라 foo ()라고 생각했습니다. 다시 말해, 만약 당신이한다면 int foo(10)같은 문제에 빠지지 않겠습니까? 둘째, 그것을 사용하지 않는 또 다른 이유는 과도한 엔지니어링 문제가되는 것 같습니다.하지만 우리가을 사용하여 모든 객체를 구성 {}하지만 언젠가 초기화 목록에 대한 생성자를 추가하면 어떻게됩니까? 이제 모든 구성 구문이 이니셜 라이저 목록 문으로 바뀝니다. 리팩토링 측면에서 매우 약한 것 같습니다. 이것에 대한 의견이 있으십니까?
void.pointer

7
@RobertDailey : "그렇다면 int foo(10)같은 문제가 발생하지 않습니까?" 10 번은 정수 리터럴이며 정수 리터럴은 유형 이름이 될 수 없습니다. vexing 구문 분석은 형식 이름 Bar()또는 임시 값일 수 있다는 사실에서 비롯됩니다 . 이것이 컴파일러에 대한 모호성을 만드는 것입니다.
Nicol Bolas

8
unpleasant behavior- 기억하기 위해 새로운 standardese 용어있다>
sehe

64

Nicol Bolas의 답변 섹션 중복 유형 이름 최소화 에 동의하지 않습니다 . 코드를 한 번 쓰고 여러 번 읽기 때문에, 우리는 그것을하는 데 걸리는 시간을 최소화하는 것을 시도해야한다 읽고 이해 코드가 아닌가하는 데 걸리는 시간 쓰기 코드를. 타이핑을 최소화하려고 시도하는 것은 잘못된 것을 최적화하려고합니다.

다음 코드를 참조하십시오.

vec3 GetValue()
{
  <lots and lots of code here>
  ...
  return {x, y, z};
}

위의 코드를 처음으로 읽는 사람은 아마도 그 문장에 도달 할 때마다 리턴 유형을 잊었을 것이기 때문에 return 문을 즉시 이해하지 못할 것입니다. 이제 리턴 유형을보고 리턴 명령문을 완전히 이해하려면 함수 시그니처로 스크롤하거나 일부 IDE 기능을 사용해야합니다.

그리고 다시 한 번 누군가 코드를 처음 읽는 사람이 실제로 구성되는 것을 이해하는 것은 쉽지 않습니다.

void DoSomething(const std::string &str);
...
const char* strValue = ...;
size_t strLen = ...;

DoSomething({strValue, strLen});

누군가가 DoSomething이 다른 문자열 유형을 지원해야한다고 결정 하고이 과부하를 추가하면 위의 코드가 중단됩니다.

void DoSomething(const CoolStringType& str);

CoolStringType에 const char * 및 size_t (std :: string처럼)를 취하는 생성자가 있으면 DoSomething ({strValue, strLen})을 호출하면 모호성 오류가 발생합니다.

실제 질문에 대한 나의 대답 :
아니요, 균일 초기화는 이전 스타일 생성자 구문의 대체물로 생각해서는 안됩니다.

그리고 나의 추론은 이것입니다 :
만약 두 개의 진술이 같은 종류의 의도를 가지고 있지 않다면, 그것들은 동일하게 보이지 않아야합니다. 두 가지 종류의 객체 초기화 개념이 있습니다.
1) 모든 항목을 가져 와서 초기화하는이 객체에 부어 넣으십시오.
2) 가이드로 제공 한 이러한 인수를 사용하여이 오브젝트를 구성하십시오.

개념 # 1 사용의 예 :

struct Collection
{
    int first;
    char second;
    double third;
};

Collection c {1, '2', 3.0};
std::array<int, 3> a {{ 1, 2, 3 }};
std::map<int, char> m { {1, '1'}, {2, '2'}, {3, '3'} };

개념 # 2 사용의 예 :

class Stairs
{
    std::vector<float> stepHeights;

public:
    Stairs(float initHeight, int numSteps, float stepHeight)
    {
        float height = initHeight;

        for (int i = 0; i < numSteps; ++i)
        {
            stepHeights.push_back(height);
            height += stepHeight;
        }
    }
};

Stairs s (2.5, 10, 0.5);

새로운 표준으로 사람들이 다음과 같이 계단을 초기화 할 수 있다는 것은 나쁜 일이라고 생각합니다.

Stairs s {2, 4, 6};

... 생성자의 의미를 난독 화하기 때문입니다. 이와 같은 초기화는 개념 # 1과 비슷하지만 그렇지 않습니다. 마치 보이는 것처럼 보이지만 세 단계의 계단 높이 값을 객체에 붓지 않습니다. 또한 더 중요한 것은 위와 같은 계단의 라이브러리 구현이 게시되고 프로그래머가 그것을 사용하고 있다면 라이브러리 구현자가 나중에 initializer_list 생성자를 Stairs에 추가하면 Stairs with Uniform Initialization을 사용하는 모든 코드가 추가됩니다 구문이 깨질 것입니다.

C ++ 커뮤니티는 Uniform Initialization이 어떻게 사용되는지, 즉 모든 초기화에서 균일하게 사용되는 것에 대한 공통의 규칙에 동의해야한다고 생각합니다. 또는 강력하게 제안하는 것처럼이 두 가지 초기화 개념을 분리하여 프로그래머의 의도를 코드.


애프터 후트 :
여기에 또 다른 이유가 있습니다. 이전 초기화를 대신하여 Uniform Initialization을 생각해서는 안되고 모든 초기화에 중괄호 표기법을 사용할 수없는 이유는 다음과 같습니다.

예를 들어, 복사를 위해 선호하는 구문은 다음과 같습니다.

T var1;
T var2 (var1);

이제 모든 초기화를 새로운 중괄호 구문으로 바꿔야보다 일관성있게 (그리고 코드가 보일 것입니다) 생각해야합니다. 그러나 유형 T가 집계이면 중괄호를 사용하는 구문이 작동하지 않습니다.

T var2 {var1}; // fails if T is std::array for example

48
"<여기에 많은 코드가 있습니다>"가 있으면 구문에 관계없이 코드를 이해하기 어렵습니다.
kevin cline

8
IMO 외에 어떤 유형을 반환하는지 알려주는 것은 IDE의 의무입니다 (예 : 호버링을 통해). 물론 IDE를 사용하지 않으면 스스로 부담을
감수해야합니다.

4
@TommiT 나는 당신이하는 말의 일부에 동의합니다. 그러나 명시 적 형식 선언auto 대 vs. 토론 과 같은 정신으로 균형을 주장합니다. 균일 한 이니셜 라이저 는 형식이 일반적으로 어쨌든 명백한 템플릿 메타 프로그래밍 상황 에서 꽤 큰 시간을 보냅니다. 간단한 oneline 함수 템플릿 (나를 울게 함)과 같은 주문에 대해 복잡한 반복을 피할 수 있습니다 . -> decltype(....)
sehe

5
" 그러나 유형 T가 집계 인 경우 중괄호를 사용하는 구문은 작동하지 않습니다 .
Nicol Bolas

5
"이제 그는 함수 서명으로 다시 스크롤해야합니다."스크롤해야 할 경우 함수가 너무 큽니다.
Miles Rout

-3

merely copy their parameters클래스 in exactly the same order내에서 선언 된 각 클래스 변수 의 생성자 가 균일 초기화를 사용하면 생성자를 호출하는 것보다 빠를 수 있습니다 (그러나 절대적으로 동일 할 수도 있음).

분명히 이것은 생성자를 항상 선언해야한다는 사실을 변경하지 않습니다.


2
왜 더 빠를 수 있다고 말합니까?
jbcoe

이것은 올바르지 않습니다. 생성자를 선언 할 필요는 없습니다 : struct X { int i; }; int main() { X x{42}; }. 균일 한 초기화가 값 초기화보다 빠를 수 있다는 것도 잘못된 것입니다.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.