Có sự khác biệt giữa khởi tạo bản sao và khởi tạo trực tiếp không?


244

Giả sử tôi có chức năng này:

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());
}

Trong mỗi nhóm, những tuyên bố này là giống hệt nhau? Hoặc có một bản sao bổ sung (có thể tối ưu hóa) trong một số khởi tạo không?

Tôi đã thấy mọi người nói cả hai điều. Xin trích dẫn văn bản làm bằng chứng. Ngoài ra thêm các trường hợp khác xin vui lòng.


1
Và có trường hợp thứ tư được thảo luận bởi @JohannesSchaub - A c1; A c2 = c1; A c3(c1);.
Dan Nissenbaum

1
Chỉ là một ghi chú năm 2018: Các quy tắc đã thay đổi trong C ++ 17 , xem, ví dụ, ở đây . Nếu sự hiểu biết của tôi là chính xác, trong C ++ 17, cả hai câu lệnh đều có hiệu quả như nhau (ngay cả khi ctor sao chép rõ ràng). Ngoài ra, nếu biểu thức init sẽ thuộc loại khác A, khởi tạo sao chép sẽ không yêu cầu tồn tại của bộ sao chép / di chuyển. Đây là lý do tại sao std::atomic<int> a = 1;ok trong C ++ 17 nhưng không phải trước đây.
Daniel Langr

Câu trả lời:


246

Cập nhật C ++ 17

Trong C ++ 17, ý nghĩa của việc A_factory_func()thay đổi từ việc tạo một đối tượng tạm thời (C ++ <= 14) sang chỉ định khởi tạo bất kỳ đối tượng nào mà biểu thức này được khởi tạo thành (nói một cách lỏng lẻo) trong C ++ 17. Các đối tượng này (được gọi là "đối tượng kết quả") là các biến được tạo bởi một khai báo (như a1), các đối tượng nhân tạo được tạo khi khởi tạo kết thúc bị loại bỏ hoặc nếu một đối tượng là cần thiết cho liên kết tham chiếu (như, trong A_factory_func();trường hợp cuối cùng, một đối tượng được tạo ra một cách giả tạo, được gọi là "vật chất hóa tạm thời", bởi vì A_factory_func()không có biến hoặc tham chiếu mà nếu không sẽ yêu cầu một đối tượng tồn tại).

Như các ví dụ trong trường hợp của chúng tôi, trong trường hợp a1a2các quy tắc đặc biệt nói rằng trong các khai báo như vậy, đối tượng kết quả của trình khởi tạo prvalue cùng loại với a1biến a1và do đó A_factory_func()trực tiếp khởi tạo đối tượng a1. Bất kỳ diễn viên kiểu chức năng trung gian nào cũng sẽ không có bất kỳ ảnh hưởng nào, bởi vì A_factory_func(another-prvalue)chỉ "đi qua" đối tượng kết quả của giá trị bên ngoài cũng là đối tượng kết quả của giá trị bên trong.


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

Phụ thuộc vào loại A_factory_func()trả về. Tôi giả sử nó trả về một A- sau đó nó cũng làm như vậy - ngoại trừ khi hàm tạo sao chép rõ ràng, thì cái đầu tiên sẽ thất bại. Đọc 8,6 / 14

double b1 = 0.5;
double b2(0.5);

Điều này cũng tương tự vì đây là loại tích hợp (điều này có nghĩa không phải là loại lớp ở đây). Đọc 8.6 / 14 .

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

Điều này không làm như vậy. Mặc định đầu tiên - khởi tạo nếu Akhông phải là POD và không thực hiện bất kỳ khởi tạo nào cho POD (Đọc 8.6 / 9 ). Bản sao thứ hai khởi tạo: Giá trị - khởi tạo tạm thời và sau đó sao chép giá trị đó vào c2(Đọc 5.2.3 / 28.6 / 14 ). Điều này tất nhiên sẽ yêu cầu một hàm tạo sao chép không rõ ràng (Đọc 8.6 / 1412.3.1 / 313.3.1.3/1 ). Cái thứ ba tạo ra một khai báo hàm cho một hàm c3trả về một Avà đưa một con trỏ hàm đến một hàm trả về một A(Đọc 8.2 ).


Đi sâu vào khởi tạo Khởi tạo trực tiếp và sao chép

Mặc dù trông giống hệt nhau và được cho là làm giống nhau, hai hình thức này khác nhau đáng kể trong một số trường hợp nhất định. Hai hình thức khởi tạo là khởi tạo trực tiếp và sao chép:

T t(x);
T t = x;

Có những hành vi chúng ta có thể quy cho mỗi người trong số họ:

  • Khởi tạo trực tiếp hoạt động giống như một hàm gọi đến một hàm bị quá tải: Các hàm, trong trường hợp này, là các hàm tạo của T(bao gồm cả explicitcác hàm) và đối số là x. Độ phân giải quá tải sẽ tìm ra hàm tạo phù hợp nhất và khi cần sẽ thực hiện bất kỳ chuyển đổi ngầm định nào được yêu cầu.
  • Sao chép khởi tạo xây dựng một chuỗi chuyển đổi ngầm định: Nó cố gắng chuyển đổi xthành một đối tượng kiểu T. (Sau đó, nó có thể sao chép đối tượng đó vào đối tượng được khởi tạo, do đó cũng cần một hàm tạo sao chép - nhưng điều này không quan trọng dưới đây)

Như bạn thấy, khởi tạo sao chép theo một cách nào đó là một phần của khởi tạo trực tiếp liên quan đến chuyển đổi ngầm có thể có: Trong khi khởi tạo trực tiếp có tất cả các hàm tạo có sẵn để gọi và ngoài ra có thể thực hiện bất kỳ chuyển đổi ngầm nào để khớp với các loại đối số, sao chép khởi tạo chỉ có thể thiết lập một chuỗi chuyển đổi ngầm định.

Tôi đã cố gắng hết sức và nhận được đoạn mã sau để xuất văn bản khác nhau cho mỗi biểu mẫu đó mà không sử dụng "hiển nhiên" thông qua các hàm explicittạo.

#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>

Làm thế nào nó hoạt động, và tại sao nó xuất kết quả đó?

  1. Khởi tạo trực tiếp

    Đầu tiên nó không biết gì về chuyển đổi. Nó sẽ chỉ cố gắng gọi một nhà xây dựng. Trong trường hợp này, hàm tạo sau có sẵn và khớp chính xác :

    B(A const&)

    Không có chuyển đổi, ít hơn một chuyển đổi do người dùng xác định, cần thiết để gọi hàm tạo đó (lưu ý rằng không có chuyển đổi định tính const nào xảy ra ở đây). Và vì vậy, khởi tạo trực tiếp sẽ gọi nó.

  2. Sao chép khởi tạo

    Như đã nói ở trên, khởi tạo sao chép sẽ xây dựng một chuỗi chuyển đổi khi achưa gõ Bhoặc xuất phát từ nó (rõ ràng là trường hợp ở đây). Vì vậy, nó sẽ tìm cách để thực hiện chuyển đổi và sẽ tìm thấy các ứng cử viên sau đây

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

    Lưu ý cách tôi viết lại hàm chuyển đổi: Kiểu tham số phản ánh loại thiscon trỏ, trong hàm không phải là thành viên không phải là hằng. Bây giờ, chúng tôi gọi những ứng cử viên này xlà đối số. Người chiến thắng là chức năng chuyển đổi: Bởi vì nếu chúng ta có hai hàm ứng cử viên, cả hai đều chấp nhận tham chiếu đến cùng loại, thì phiên bản const ít hơn sẽ thắng (đây cũng là cơ chế ưu tiên các hàm không phải là thành viên không gọi đối tượng -const).

    Lưu ý rằng nếu chúng ta thay đổi hàm chuyển đổi thành hàm const thành viên, thì chuyển đổi không rõ ràng (vì cả hai đều có kiểu tham số A const&sau đó): Trình biên dịch Comeau từ chối nó một cách chính xác, nhưng GCC chấp nhận nó ở chế độ không phạm vi. Tuy nhiên, chuyển sang -pedanticlàm cho nó đưa ra cảnh báo mơ hồ thích hợp.

Tôi hy vọng điều này sẽ giúp phần nào làm cho nó rõ ràng hơn về hai hình thức này khác nhau như thế nào!


Ồ Tôi thậm chí không nhận ra về khai báo chức năng. Tôi gần như phải chấp nhận câu trả lời của bạn chỉ vì là người duy nhất biết về điều đó. Có một lý do mà khai báo hàm làm việc theo cách đó? Sẽ tốt hơn nếu c3 được xử lý khác nhau bên trong một chức năng.
rlbond

4
Bah, xin lỗi mọi người, nhưng tôi đã phải xóa bình luận của mình và đăng lại, vì công cụ định dạng mới: Đó là vì trong các tham số chức năng, R() == R(*)()T[] == T*. Nghĩa là, các kiểu hàm là các kiểu con trỏ hàm và các kiểu mảng là các kiểu con trỏ-thành phần. Điều này thật tệ Nó có thể được xử lý xung quanh A c3((A()));(parens xung quanh biểu thức).
Julian Schaub - litb

4
Tôi có thể hỏi "'Đọc 8,5 / 14'" nghĩa là gì không? Điều đó đề cập đến điều gì? Một quyển sách? Một chương? Một trang web?
AzP

9
@AzP nhiều người trên SO thường muốn tham khảo thông số kỹ thuật C ++ và đó là những gì tôi đã làm ở đây, để đáp lại yêu cầu của rlbond "Vui lòng trích dẫn văn bản làm bằng chứng." Tôi không muốn trích dẫn thông số kỹ thuật, vì điều đó làm nổi lên câu trả lời của tôi và còn rất nhiều việc phải cập nhật (dự phòng).
Julian Schaub - litb

1
@luca tôi khuyên bạn nên bắt đầu một câu hỏi mới để người khác có thể hưởng lợi từ câu trả lời mà mọi người đưa ra
Julian Schaub - litb

49

Bài tập khác với khởi tạo .

Cả hai dòng sau đây đều khởi tạo . Một lệnh gọi constructor được thực hiện:

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

nhưng nó không tương đương với:

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

Tôi không có một văn bản tại thời điểm này để chứng minh điều này nhưng nó rất dễ để thử nghiệm:

#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
Tài liệu tham khảo tốt: "Ngôn ngữ lập trình C ++, phiên bản đặc biệt" của Bjarne Stroustrup, phần 10.4.4.1 (trang 245). Mô tả khởi tạo sao chép và gán sao chép và tại sao chúng khác nhau về cơ bản (mặc dù cả hai đều sử dụng toán tử = làm cú pháp).
Naaff

Nit nhỏ, nhưng tôi thực sự không thích khi mọi người nói rằng "A a (x)" và "A a = x" bằng nhau. Nghiêm túc họ không. Trong nhiều trường hợp, chúng sẽ thực hiện chính xác cùng một thứ nhưng có thể tạo các ví dụ trong đó tùy thuộc vào đối số, các hàm tạo khác nhau thực sự được gọi.
Richard Corden

Tôi không nói về "tương đương cú pháp." Về mặt ngữ nghĩa, cả hai cách khởi tạo đều giống nhau.
Mehrdad Afshari

@MehrdadAfshari Trong mã câu trả lời của Julian, bạn nhận được đầu ra khác nhau dựa trên hai trong số bạn sử dụng.
Brian Gordon

1
@BrianGordon Vâng, bạn nói đúng. Chúng không tương đương. Tôi đã giải quyết bình luận của Richard trong bản chỉnh sửa của tôi từ lâu.
Mehrdad Afshari

22

double b1 = 0.5; là cuộc gọi ngầm của nhà xây dựng.

double b2(0.5); là cuộc gọi rõ ràng.

Nhìn vào đoạn mã sau để thấy sự khác biệt:

#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;
}

Nếu lớp của bạn không có bộ tạo rõ ràng hơn các cuộc gọi rõ ràng và ẩn là giống hệt nhau.


5
+1. Câu trả lời tốt đẹp. Tốt để lưu ý các phiên bản rõ ràng. Nhân tiện, điều quan trọng cần lưu ý là bạn không thể có cả hai phiên bản của một công cụ xây dựng quá tải cùng một lúc. Vì vậy, nó sẽ không biên dịch trong trường hợp rõ ràng. Nếu cả hai cùng biên dịch, họ phải cư xử tương tự nhau.
Mehrdad Afshari

4

Nhóm đầu tiên: nó phụ thuộc vào những gì A_factory_functrả về. Dòng đầu tiên là một ví dụ về khởi tạo bản sao , dòng thứ hai là khởi tạo trực tiếp . Nếu A_factory_functrả về một Ađối tượng thì chúng tương đương, cả hai đều gọi hàm tạo sao chép A, nếu không, phiên bản đầu tiên tạo ra một giá trị loại Atừ một toán tử chuyển đổi có sẵn cho kiểu trả về của các hàm tạo A_factory_functhích hợp hoặc Asau đó gọi hàm tạo sao chép để xây dựng a1từ đây tạm thời Phiên bản thứ hai cố gắng tìm một hàm tạo phù hợp nhận bất kỳ giá trị A_factory_functrả về nào hoặc lấy thứ gì đó mà giá trị trả về có thể được chuyển đổi hoàn toàn thành.

Nhóm thứ hai: chính xác cùng một logic giữ, ngoại trừ việc được xây dựng trong các loại không có bất kỳ nhà xây dựng kỳ lạ nào, trong thực tế, chúng giống hệt nhau.

Nhóm thứ ba: c1được khởi tạo mặc định, c2được khởi tạo sao chép từ một giá trị được khởi tạo tạm thời. Bất kỳ thành viên c1nào có kiểu nhóm (hoặc thành viên của các thành viên, v.v.) có thể không được khởi tạo nếu người dùng cung cấp các hàm tạo mặc định (nếu có) không khởi tạo rõ ràng chúng. Đối với c2, nó phụ thuộc vào việc có một người xây dựng bản sao được cung cấp bởi người dùng hay không và liệu điều đó có khởi tạo một cách thích hợp các thành viên đó hay không, nhưng tất cả các thành viên tạm thời sẽ được khởi tạo (không khởi tạo nếu không được khởi tạo rõ ràng). Như litb phát hiện, c3là một cái bẫy. Đây thực sự là một khai báo hàm.


4

Chú ý:

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

Tức là, để khởi tạo bản sao.

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

Nói cách khác, một trình biên dịch tốt sẽ không tạo ra một bản sao để khởi tạo sao chép khi có thể tránh được; thay vào đó, nó sẽ chỉ gọi hàm tạo trực tiếp - tức là giống như khởi tạo trực tiếp.

Nói cách khác, khởi tạo sao chép giống như khởi tạo trực tiếp trong hầu hết các trường hợp <ý kiến> trong đó mã dễ hiểu đã được viết. Vì khởi tạo trực tiếp có khả năng gây ra các chuyển đổi tùy ý (và do đó có thể chưa biết), tôi thích luôn luôn sử dụng khởi tạo sao chép khi có thể. (Với phần thưởng mà nó thực sự trông giống như khởi tạo.) </ Ý kiến>

Kỹ thuật goriness: [12.2 / 1 cont từ trên] 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.

Vui vì tôi không viết trình biên dịch C ++.


4

Bạn có thể thấy sự khác biệt của nó trong explicitimplicitloại constructor khi bạn khởi tạo một đối tượng:

Các lớp học :

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

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

Và trong main chức năng:

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
}

Theo mặc định, một hàm tạo là như implicitvậy, bạn có hai cách để khởi tạo nó:

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

Và bằng cách định nghĩa một cấu trúc như explicitbạn chỉ có một cách là trực tiếp:

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

Trả lời về phần này:

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

Vì hầu hết các câu trả lời là tiền c ++ 11 nên tôi đang thêm những gì c ++ 11 nói về điều này:

Một specifier-type-specifier (7.1.6.2) hoặc typename-specifier (14.6) theo sau là một danh sách biểu thức được ngoặc đơn xây dựng một giá trị của kiểu đã chỉ định cho danh sách biểu thức. Nếu danh sách biểu thức là một biểu thức đơn, biểu thức chuyển đổi loại là tương đương (theo định nghĩa và nếu được định nghĩa theo nghĩa) với biểu thức truyền tương ứng (5.4). Nếu loại được chỉ định là một loại lớp, loại lớp sẽ được hoàn thành. Nếu danh sách biểu thức chỉ định nhiều hơn một giá trị, loại sẽ là một lớp có hàm tạo được khai báo phù hợp (8.5, 12.1) và biểu thức T (x1, x2, ...) tương đương với khai báo T t (x1, x2, ...); đối với một số biến t tạm thời được phát minh, với kết quả là giá trị của t là một giá trị.

Vì vậy, tối ưu hóa hay không chúng là tương đương theo tiêu chuẩn. Lưu ý rằng điều này phù hợp với những gì câu trả lời khác đã đề cập. Chỉ cần trích dẫn những gì các tiêu chuẩn đã nói cho chính xác.


Cả "danh sách biểu thức" của ví dụ của bạn chỉ định nhiều hơn một giá trị ". Làm thế nào là bất kỳ điều này có liên quan?
gạch dưới

0

Rất nhiều trong số các trường hợp này là đối tượng thực hiện của một đối tượng, vì vậy thật khó để đưa ra câu trả lời cụ thể.

Xem xét trường hợp

A a = 5;
A a(5);

Trong trường hợp này, giả sử một toán tử gán và khởi tạo hàm thích hợp chấp nhận một đối số nguyên duy nhất, cách tôi triển khai các phương thức đã nói ảnh hưởng đến hành vi của từng dòng. Tuy nhiên, đó là một thực tế phổ biến đối với một trong những người gọi người khác trong quá trình thực hiện là loại bỏ mã trùng lặp (mặc dù trong trường hợp đơn giản như thế này sẽ không có mục đích thực sự.)

Chỉnh sửa: Như đã đề cập trong các phản hồi khác, trên thực tế, dòng đầu tiên sẽ gọi hàm tạo sao chép. Hãy xem xét các ý kiến ​​liên quan đến toán tử gán như là hành vi liên quan đến một nhiệm vụ độc lập.

Điều đó nói rằng, cách trình biên dịch tối ưu hóa mã sau đó sẽ có tác động riêng của nó. Nếu tôi có hàm khởi tạo đang gọi toán tử "=" - nếu trình biên dịch không tối ưu hóa, dòng trên cùng sẽ thực hiện 2 lần nhảy so với một trong dòng dưới cùng.

Bây giờ, đối với các tình huống phổ biến nhất, trình biên dịch của bạn sẽ tối ưu hóa thông qua các trường hợp này và loại bỏ loại không hiệu quả này. Vì vậy, hiệu quả tất cả các tình huống khác nhau mà bạn mô tả sẽ giống nhau. Nếu bạn muốn xem chính xác những gì đang được thực hiện, bạn có thể xem mã đối tượng hoặc đầu ra lắp ráp của trình biên dịch của bạn.


Nó không phải là một tối ưu hóa . Trình biên dịch phải gọi hàm tạo giống nhau trong cả hai trường hợp. Kết quả là, không ai trong số họ sẽ biên dịch nếu bạn chỉ có operator =(const int)và không có A(const int). Xem câu trả lời của @ jia3ep để biết thêm chi tiết.
Mehrdad Afshari

Tôi tin rằng bạn thực sự chính xác. Tuy nhiên, nó sẽ biên dịch tốt bằng cách sử dụng một hàm tạo sao chép mặc định.
dborba

Ngoài ra, như tôi đã đề cập, thông thường có một hàm tạo sao chép gọi một toán tử gán, tại đó tối ưu hóa trình biên dịch sẽ phát huy tác dụng.
dborba

0

Đây là từ Ngôn ngữ lập trình C ++ của Bjarne Stroustrup:

Khởi tạo với an = được coi là khởi tạo sao chép . Về nguyên tắc, một bản sao của trình khởi tạo (đối tượng chúng ta đang sao chép) được đặt vào đối tượng khởi tạo. Tuy nhiên, một bản sao như vậy có thể được tối ưu hóa (bỏ qua) và thao tác di chuyển (dựa trên ngữ nghĩa di chuyển) có thể được sử dụng nếu trình khởi tạo là một giá trị. Rời khỏi = làm cho việc khởi tạo rõ ràng. Khởi tạo rõ ràng được gọi là khởi tạo trực tiếp .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.