복사 초기화와 직접 초기화간에 차이가 있습니까?


244

이 기능이 있다고 가정하십시오.

void my_test()
{
    A a1 = A_factory_func();
    A a2(A_factory_func());

    double b1 = 0.5;
    double b2(0.5);

    A c1;
    A c2 = A();
    A c3(A());
}

각 그룹에서 이러한 진술은 동일합니까? 아니면 일부 초기화에 추가 (최적화 가능) 사본이 있습니까?

사람들이 두 가지를 모두 말하는 것을 보았습니다. 텍스트를 증거로 인용 하십시오 . 다른 경우도 추가하십시오.


1
그리고 @JohannesSchaub-에 의해 논의 된 네 번째 경우가 A c1; A c2 = c1; A c3(c1);있습니다.
Dan Nissenbaum

1
단지 2018 참고 사항 : 규칙은 C ++ 17 에서 변경되었습니다 ( 예 : here 참조) . 내 이해가 정확하다면 C ++ 17에서는 두 문장이 사실상 동일합니다 (복사본이 명시 적 인 경우에도). 또한 init 표현식이 이외의 유형 인 경우 A복사 초기화에는 복사 / 이동 구성자가 필요하지 않습니다. 이것이 std::atomic<int> a = 1;C ++ 17에서는 괜찮지 만 이전에는 그렇지 않은 이유 입니다.
Daniel Langr

답변:


246

C ++ 17 업데이트

C ++ 17에서는 A_factory_func()임시 객체 생성 (C ++ <= 14)에서 C ++ 17에서이 표현식이 초기화 된 (느슨하게 말하면) 객체의 초기화를 지정하는 것으로 변경되었습니다. 이러한 객체 ( "결과 객체"라고 함)는 선언 (예 a1:), 초기화가 종료 될 때 생성 된 인공 객체 또는 참조 바인딩에 객체가 필요한 A_factory_func();경우 (예 : 개체가 A_factory_func()존재해야하는 변수 나 참조가 없기 때문에 "임시 구체화"라는 개체가 인위적으로 생성 됩니다.

우리의 경우 예로서,의 경우 a1a2특별한 규칙 같은 선언에서, 같은 유형의 초기화 prvalue의 결과 개체 말할 a1변수 a1때문에, 그리고 A_factory_func()직접적으로 개체를 초기화합니다 a1. A_factory_func(another-prvalue)외부 prvalue의 결과 객체를 내부 prvalue의 결과 객체로 "통과" 하기 만하면 중개 기능 스타일 캐스트는 아무런 영향을 미치지 않습니다 .


A a1 = A_factory_func();
A a2(A_factory_func());

A_factory_func()반환 되는 유형에 따라 다릅니다. A복사 생성자가 명시 적 인 경우 첫 번째 생성자가 실패한다는 점을 제외하고 는 -를 반환한다고 가정합니다 . 8.6 / 14 읽기

double b1 = 0.5;
double b2(0.5);

이것은 내장 타입이기 때문에 똑같이하고 있습니다 (여기서는 클래스 유형이 아닙니다). 8.6 / 14를 읽으십시오 .

A c1;
A c2 = A();
A c3(A());

이것은 똑같이하지 않습니다. 첫 번째 A는 비 POD 인 경우 기본적으로 초기화되며 POD에 대한 초기화를 수행하지 않습니다 ( 8.6 / 9 읽기 ). 두 번째 사본은 다음을 초기화합니다. 값을 임시로 초기화 한 다음 해당 값을 c2( 5.2.3 / 28.6 / 14 읽기)에 복사합니다 . 물론 명시 적이 아닌 복사 생성자가 필요합니다 ( 8.6 / 1412.3.1 / 313.3.1.3/1 읽기 ). 세 번째는 a c3를 반환하고 a 를 반환하는 A함수에 대한 함수 포인터를받는 함수에 대한 함수 선언을 만듭니다 A( 8.2 읽기 ).


초기화 직접 및 복사 초기화

그것들은 동일하게 보이고 동일하게 수행되어야하지만,이 두 형태는 경우에 따라 현저히 다릅니다. 초기화의 두 가지 형태는 직접 및 복사 초기화입니다.

T t(x);
T t = x;

우리는 그들 각각에 귀속되는 행동이 있습니다 :

  • 직접 초기화는 오버로드 된 함수에 대한 함수 호출처럼 작동합니다.이 경우 함수는 (함수 T포함 explicit) 의 생성자이며 인수는 다음과 같습니다.x 입니다. 과부하 해결은 가장 일치하는 생성자를 찾고 필요할 때 암시 적 변환이 필요합니다.
  • 복사 초기화는 암시 적 변환 시퀀스를 구성합니다 . x유형의 객체 로 변환 을 시도 합니다 T. 그런 다음 해당 객체를 초기화 된 객체로 복사 할 수 있으므로 복사 생성자도 필요하지만 아래에서는 중요하지 않습니다.

보시다시피, 복사 초기화 는 어떤 식 으로든 가능한 암시 적 변환과 관련하여 직접 초기화의 일부입니다. 직접 초기화에는 모든 생성자가 호출 가능 하며 추가로 가 인수 형식을 일치 할 필요가있는 암시 적 변환을 수행 할 수 있습니다, 초기화를 복사 하나의 암시 적 변환 시퀀스를 설정할 수 있습니다.

나는 열심히 노력 하여 생성자를 통해 "명백한"을 사용하지 않고 각 양식에 대해 다른 텍스트를 출력하는 다음 코드를 얻었습니다explicit .

#include <iostream>
struct B;
struct A { 
  operator B();
};

struct B { 
  B() { }
  B(A const&) { std::cout << "<direct> "; }
};

A::operator B() { std::cout << "<copy> "; return B(); }

int main() { 
  A a;
  B b1(a);  // 1)
  B b2 = a; // 2)
}
// output: <direct> <copy>

어떻게 작동하고 왜 결과를 출력합니까?

  1. 직접 초기화

    먼저 전환에 대해 아무것도 모릅니다. 생성자를 호출하려고 시도합니다. 이 경우 다음 생성자를 사용할 수 있으며 정확히 일치합니다 .

    B(A const&)

    해당 생성자를 호출하는 데 필요한 변환은 사용자 정의 변환보다 훨씬 적습니다 (여기서 const 한정 변환도 수행되지 않음). 따라서 직접 초기화가 호출됩니다.

  2. 복사 초기화

    위에서 언급했듯이, 복사 초기화는 a유형이 B없거나 파생 되지 않은 경우 변환 시퀀스를 구성 합니다 (여기서는 분명히 그렇습니다). 변환을 수행하는 방법을 찾고 다음 후보를 찾습니다.

    B(A const&)
    operator B(A&);

    변환 함수를 어떻게 다시 작성했는지 주목하십시오. 매개 변수 유형은 this포인터가 아닌 유형을 나타내는 포인터 유형을 반영합니다 . 이제 우리는이 후보들을 x논쟁으로 부릅니다 . 승자가 변환 함수입니다. 두 개의 후보 함수가 모두 동일한 유형에 대한 참조를 허용하는 경우 더 적은 const 버전 (비 const 멤버 함수를 선호하는 메커니즘은 -const 객체).

    변환 함수를 const 멤버 함수로 변경하면 변환이 모호합니다 (둘 다 매개 변수 유형이 되었기 때문에 A const&). -pedantic그래도 전환 하면 적절한 모호성 경고가 출력됩니다.

이 두 가지 형식이 어떻게 다른지 더 명확하게 만드는 데 도움이되기를 바랍니다.


와. 나는 함수 선언에 대해조차 몰랐다. 나는 그것에 대해 아는 유일한 사람이기 때문에 당신의 대답을 받아 들여야합니다. 함수 선언이 그렇게 작동하는 이유가 있습니까? c3가 함수 내에서 다르게 처리되면 더 좋습니다.
rlbond 2016 년

4
이 때문에 함수 매개 변수에있어, : 바하마, 죄송 사람,하지만 난 때문에 새로운 포맷 엔진의 내 주석을 제거하고 다시 게시했다 R() == R(*)()T[] == T*. 즉, 함수 유형은 함수 포인터 유형이고 배열 유형은 요소에 대한 포인터 유형입니다. 짜증나 A c3((A()));(표현식에 따라 다름)으로 해결할 수 있습니다 .
Johannes Schaub-litb

4
" '8.5 / 14 읽기'가 무엇을 의미하는지 물어봐도 될까요? 그것은 무엇을 의미합니까? 책? 챕터? 웹 사이트?
AzP

9
@AzP SO에 많은 사람들이 종종 C ++ 사양에 대한 참조를 원하며, rlbond의 요청 "텍스트를 증거로 인용하십시오."에 대한 응답으로 내가 여기서 한 일입니다. 사양을 인용하고 싶지 않습니다. 왜냐하면 그 대답이 부풀어 오르고 최신 상태를 유지하는 데 훨씬 많은 작업 (중복성)이기 때문입니다.
Johannes Schaub-litb

1
@luca 나는 새로운 질문을 시작하여 다른 사람들이 사람들이주는 대답으로부터 혜택을받을 수 있도록 권장합니다
Johannes Schaub-litb

49

할당초기화 와 다릅니다 .

다음 두 줄 모두 초기화를 수행 합니다. 단일 생성자 호출이 수행됩니다.

A a1 = A_factory_func();  // calls copy constructor
A a1(A_factory_func());   // calls copy constructor

그러나 다음과 같습니다.

A a1;                     // calls default constructor
a1 = A_factory_func();    // (assignment) calls operator =

현재 이것을 증명할 텍스트가 없지만 실험하기는 매우 쉽습니다.

#include <iostream>
using namespace std;

class A {
public:
    A() { 
        cout << "default constructor" << endl;
    }

    A(const A& x) { 
        cout << "copy constructor" << endl;
    }

    const A& operator = (const A& x) {
        cout << "operator =" << endl;
        return *this;
    }
};

int main() {
    A a;       // default constructor
    A b(a);    // copy constructor
    A c = a;   // copy constructor
    c = b;     // operator =
    return 0;
}

2
참조 : Bjarne Stroustrup의 "C ++ 프로그래밍 언어, 스페셜 에디션"섹션 10.4.4.1 (245 페이지). 복사 초기화 및 복사 할당과 근본적으로 다른 이유를 설명합니다 (두 연산자 모두 = 연산자를 구문으로 사용하지만).
Naaff

사소한 니트이지만 사람들이 "A a (x)"와 "A a = x"가 같다고 말하는 것을 정말로 좋아하지 않습니다. 엄격히 그들은 아닙니다. 많은 경우에 그들은 정확히 똑같은 일을 할 것이지만 인수에 따라 다른 생성자가 실제로 호출되는 예제를 만들 수 있습니다.
Richard Corden 2016 년

나는 "구문 적 동등성"에 대해 말하는 것이 아닙니다. 의미 적으로, 두 가지 초기화 방법은 동일합니다.
Mehrdad Afshari 2016 년

@MehrdadAfshari Johannes의 답변 코드에서 두 가지 중 어떤 것을 사용 하느냐에 따라 다른 출력을 얻습니다.
Brian Gordon

1
@BrianGordon 그래, 맞아. 그것들은 동등하지 않습니다. 나는 오래 전에 편집에서 Richard의 의견을 언급했다.
Mehrdad Afshari

22

double b1 = 0.5; 암시 적 생성자의 호출입니다.

double b2(0.5); 명시 적 호출입니다.

차이점을 보려면 다음 코드를보십시오.

#include <iostream>
class sss { 
public: 
  explicit sss( int ) 
  { 
    std::cout << "int" << std::endl;
  };
  sss( double ) 
  {
    std::cout << "double" << std::endl;
  };
};

int main() 
{ 
  sss ddd( 7 ); // calls int constructor 
  sss xxx = 7;  // calls double constructor 
  return 0;
}

클래스에 명시 적 구성자가없는 경우 명시 적 호출과 내재적 호출이 동일합니다.


5
+1. 좋은 대답입니다. 명시 적 버전도 참고하십시오. 그건 그렇고, 단일 생성자 과부하의 버전을 동시에 가질 수는 없습니다 . 따라서 명시적인 경우에는 컴파일에 실패합니다. 둘 다 컴파일하면 비슷하게 동작해야합니다.
Mehrdad Afshari 2016 년

4

첫 번째 그룹화 : A_factory_func반환되는 항목에 따라 다릅니다 . 첫 번째 줄은 복사 초기화 의 예이고 두 번째 줄은 직접 초기화 입니다. 경우 A_factory_func다시 표시 A한 후 그들이 동일 객체는 둘 다 전화의 복사 생성자 A, 그렇지 않으면 첫 번째 버전은 유형의를 rvalue 만들어 A반환의 유형에 대해 사용 가능한 변환 사업자 A_factory_func또는 적절한 A생성자를 한 다음 구조에 복사 생성자를 호출 a1이에서 일시적인. 두 번째 버전은 무엇이든 가져 오는 적합한 생성자를 찾으려고합니다.A_factory_func 은 리턴 값을 가져 오거나 리턴 값을 내재적으로 변환 할 수있는 무언가를 .

두 번째 그룹화 : 내장 유형에는 이국적인 생성자가 없으므로 실제로 동일하다는 점을 제외하고는 정확히 동일한 논리가 유지됩니다.

세 번째 그룹화 : c1기본적으로 초기화되며 c2임시로 초기화 된 값에서 복사 초기화됩니다. 의 모든 구성원 c1사용자 공급 기본 생성자 (있는 경우)를 명시 적으로 초기화하지 않는 경우 초기화되지 않을 수 있습니다 (등 등 또는 회원의 회원,) 포드 형이있다. 의 경우 c2사용자 제공 복사 생성자가 있는지 여부와 해당 멤버를 적절하게 초기화하는지 여부에 따라 달라 지지만 임시 멤버는 모두 초기화됩니다 (명시 적으로 초기화되지 않은 경우 0으로 초기화 됨). litb가 발견 한대로 c3함정입니다. 실제로 함수 선언입니다.


4

참고 사항 :

[12.2 / 1] Temporaries of class type are created in various contexts: ... and in some initializations (8.5).

즉, 복사 초기화에 사용됩니다.

[12.8 / 15] When certain criteria are met, an implementation is allowed to omit the copy construction of a class object ...

다시 말해, 좋은 컴파일러는 피할 수있을 때 복사 초기화를위한 사본을 만들지 않습니다 . 대신 직접 초기화와 마찬가지로 생성자를 직접 호출합니다.

다시 말해, 복사 초기화는 이해하기 쉬운 코드가 작성된 대부분의 경우 <opinion>의 직접 초기화와 같습니다. 직접 초기화는 잠재적으로 임의 (따라서 알려지지 않은) 변환을 일으킬 수 있으므로 가능하면 항상 복사 초기화를 사용하는 것을 선호합니다. (실제로 초기화하는 것처럼 보이는 보너스로) </ opinion>

기술적 인 고요함 : [12.2 / 1 위에서 계속] Even when the creation of the temporary object is avoided (12.8), all the semantic restrictions must be respected as if the temporary object was created.

C ++ 컴파일러를 작성하지 않고 다행입니다.


4

객체를 초기화 할 때 생성자 explicitimplicit생성자 유형 의 차이를 확인할 수 있습니다 .

클래스 :

class A
{
    A(int) { }      // converting constructor
    A(int, int) { } // converting constructor (C++11)
};

class B
{
    explicit B(int) { }
    explicit B(int, int) { }
};

그리고 main 기능에서 :

int main()
{
    A a1 = 1;      // OK: copy-initialization selects A::A(int)
    A a2(2);       // OK: direct-initialization selects A::A(int)
    A a3 {4, 5};   // OK: direct-list-initialization selects A::A(int, int)
    A a4 = {4, 5}; // OK: copy-list-initialization selects A::A(int, int)
    A a5 = (A)1;   // OK: explicit cast performs static_cast

//  B b1 = 1;      // error: copy-initialization does not consider B::B(int)
    B b2(2);       // OK: direct-initialization selects B::B(int)
    B b3 {4, 5};   // OK: direct-list-initialization selects B::B(int, int)
//  B b4 = {4, 5}; // error: copy-list-initialization does not consider B::B(int,int)
    B b5 = (B)1;   // OK: explicit cast performs static_cast
}

기본적으로 생성자는 implicit초기화하는 두 가지 방법이 있습니다.

A a1 = 1;        // this is copy initialization
A a2(2);         // this is direct initialization

그리고 explicit하나의 방법으로 직접 구조를 정의함으로써 :

B b2(2);        // this is direct initialization
B b5 = (B)1;    // not problem if you either use of assign to initialize and cast it as static_cast

3

이 부분에 대한 답변 :

A c2 = A (); c3 (A ());

대부분의 답변은 c-++ 11 이전이므로 c ++ 11이 이것에 대해 말한 것을 추가하고 있습니다.

단순 유형 지정자 (7.1.6.2) 또는 유형 이름 지정자 (14.6) 다음에 괄호로 묶인 expression-list는 표현식 목록에 지정된 유형의 값을 구성합니다. 표현식 목록이 단일 표현식 인 경우 유형 변환 표현식은 해당 캐스트 표현식 (5.4)과 동일합니다 (정의 된 의미로 정의 된 경우). 지정된 유형이 클래스 유형 인 경우 클래스 유형이 완료됩니다. 만약 표현리스트가 하나 이상의 값을 지정한다면, 그 유형은 적절하게 선언 된 생성자 (8.5, 12.1)를 가진 클래스가되어야하고, 표현 T (x1, x2, ...)는 선언 T t와 사실상 동일하다 (x1, x2, ...); 일부는 임시 변수 t를 발명했으며, 결과는 t 값을 prvalue로 사용합니다.

따라서 최적화 여부는 표준에 따라 동일합니다. 이것은 다른 답변에서 언급 한 내용과 일치합니다. 정확성을 위해 표준이 말한 것을 인용하십시오.


예제의 "표현 목록은 단일 값 이상을 지정하지 않습니다". 이 중 어떤 것이 관련이 있습니까?
underscore_d

0

이러한 경우는 대부분 개체의 구현에 따라 달라 지므로 구체적인 대답을하기가 어렵습니다.

사건을 고려

A a = 5;
A a(5);

이 경우 단일 정수 인수를 허용하는 적절한 할당 연산자 및 초기화 생성자를 가정하면, 상기 메소드를 구현하는 방법은 각 행의 동작에 영향을 미칩니다. 그러나 중복 코드를 제거하기 위해 구현에서 다른 하나를 호출하는 것이 일반적인 관행입니다 (단순한 경우 실제 목적은 없습니다).

편집 : 다른 응답에서 언급했듯이 첫 번째 줄은 실제로 복사 생성자를 호출합니다. 할당 연산자와 관련된 주석은 독립형 할당과 관련된 동작으로 간주하십시오.

즉, 컴파일러가 코드를 최적화하는 방법은 자체 영향을 미칩니다. "="연산자를 호출하는 초기화 생성자가있는 경우-컴파일러에서 최적화를 수행하지 않으면 맨 위 줄이 맨 아래 줄과 달리 두 번의 점프를 수행합니다.

이제 가장 일반적인 상황에서 컴파일러는 이러한 경우를 최적화하여 이러한 유형의 비 효율성을 제거합니다. 따라서 당신이 묘사하는 다른 모든 상황은 사실상 똑같을 것입니다. 정확히 무슨 일이 일어나고 있는지 보려면 컴파일러의 객체 코드 또는 어셈블리 출력을 볼 수 있습니다.


최적화 가 아닙니다 . 컴파일러 두 경우 모두 생성자를 동일 하게 호출해야합니다. 결과적으로 방금 가지고있는 경우 아무도 컴파일 operator =(const int)하지 않습니다 A(const int). 자세한 내용은 @ jia3ep의 답변을 참조하십시오.
Mehrdad Afshari 2016 년

나는 당신이 실제로 옳다고 생각합니다. 그러나 기본 복사 생성자를 사용하면 제대로 컴파일됩니다.
dborba 2016 년

또한 앞에서 언급했듯이 복사 생성자가 할당 연산자를 호출하여 컴파일러 최적화가 작동하는 것이 일반적입니다.
dborba 2016 년

0

Bjarne Stroustrup의 C ++ 프로그래밍 언어에서 가져온 것입니다.

=로 초기화 하면 복사 초기화 로 간주됩니다 . 원칙적으로, 이니셜 라이저 (복사하려는 객체)의 사본이 초기화 된 객체에 배치됩니다. 그러나, 그러한 카피는 최적화 (제거) 될 수 있고, 이니셜 라이저가 r 값이면 이동 동작 (이동 의미론에 기초한)이 사용될 수있다. =를 생략하면 초기화가 명시 적으로됩니다. 명시 적 초기화를 직접 초기화라고 합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.