Khi nào sử dụng trình khởi tạo có dấu ngoặc nhọn?


94

Trong C ++ 11, chúng ta có cú pháp mới để khởi tạo các lớp mang lại cho chúng ta một số lượng lớn các khả năng về cách khởi tạo biến.

{ // Example 1
  int b(1);
  int a{1};
  int c = 1;
  int d = {1};
}
{ // Example 2
  std::complex<double> b(3,4);
  std::complex<double> a{3,4};
  std::complex<double> c = {3,4};
  auto d = std::complex<double>(3,4);
  auto e = std::complex<double>{3,4};
}
{ // Example 3
  std::string a(3,'x');
  std::string b{3,'x'}; // oops
}
{ // Example 4
  std::function<int(int,int)> a(std::plus<int>());
  std::function<int(int,int)> b{std::plus<int>()};
}
{ // Example 5
  std::unique_ptr<int> a(new int(5));
  std::unique_ptr<int> b{new int(5)};
}
{ // Example 6
  std::locale::global(std::locale("")); // copied from 22.4.8.3
  std::locale::global(std::locale{""});
}
{ // Example 7
  std::default_random_engine a {}; // Stroustrup's FAQ
  std::default_random_engine b;
}
{ // Example 8
  duration<long> a = 5; // Stroustrup's FAQ too
  duration<long> b(5);
  duration<long> c {5};
}

Đối với mỗi biến tôi khai báo, tôi phải nghĩ mình nên sử dụng cú pháp khởi tạo nào và điều này làm chậm tốc độ viết mã của tôi. Tôi chắc rằng đó không phải là ý định giới thiệu các dấu ngoặc nhọn.

Khi nói đến mã mẫu, việc thay đổi cú pháp có thể dẫn đến các ý nghĩa khác nhau, vì vậy đi đúng cách là điều cần thiết.

Tôi tự hỏi liệu có một hướng dẫn chung nào mà người ta nên chọn cú pháp không.


1
Ví dụ về hành vi không mong muốn từ {} khởi tạo: string (50, 'x') so với chuỗi {50, 'x'} tại đây
P i

Câu trả lời:


64

Tôi nghĩ những điều sau đây có thể là một hướng dẫn tốt:

  • Nếu giá trị (đơn) mà bạn đang khởi tạo nhằm là giá trị chính xác của đối tượng, hãy sử dụng =khởi tạo copy ( ) (vì sau đó trong trường hợp có lỗi, bạn sẽ không bao giờ vô tình gọi một hàm tạo rõ ràng, thường diễn giải giá trị được cung cấp khác nhau). Ở những nơi không có khởi tạo sao chép, hãy xem liệu khởi tạo dấu ngoặc nhọn có đúng ngữ nghĩa không, và nếu có, hãy sử dụng điều đó; nếu không, hãy sử dụng khởi tạo dấu ngoặc đơn (nếu điều đó cũng không khả dụng, bạn vẫn gặp may).

  • Nếu các giá trị bạn đang khởi tạo là danh sách các giá trị được lưu trữ trong đối tượng (như các phần tử của vectơ / mảng hoặc phần thực / ảo của một số phức), hãy sử dụng khởi tạo dấu ngoặc nhọn nếu có.

  • Nếu các giá trị bạn đang khởi tạo không phải là các giá trị được lưu trữ, nhưng mô tả giá trị / trạng thái dự định của đối tượng, hãy sử dụng dấu ngoặc đơn. Ví dụ là đối số kích thước của a vectorhoặc đối số tên tệp của an fstream.


4
@ user1304032: Ngôn ngữ không phải là một chuỗi, do đó bạn sẽ không sử dụng khởi tạo sao chép. Một ngôn ngữ cũng không chứa một chuỗi (nó có thể lưu chuỗi đó dưới dạng chi tiết triển khai, nhưng đó không phải là mục đích của nó), do đó bạn sẽ không sử dụng khởi tạo dấu ngoặc nhọn. Do đó, hướng dẫn nói rằng sử dụng khởi tạo dấu ngoặc đơn.
celtschk

2
Cá nhân tôi thích hướng dẫn này nhất và nó cũng hoạt động tốt trên mã chung. Có một số ngoại lệ ( T {}hoặc lý do cú pháp như phân tích cú pháp khó chịu nhất ), nhưng nói chung tôi nghĩ đây là một lời khuyên tốt. Lưu ý rằng đây là ý kiến ​​chủ quan của tôi, vì vậy bạn cũng nên xem các câu trả lời khác.
helami

2
@celtschk: Điều đó sẽ không hoạt động đối với các loại không thể sao chép, không thể di chuyển; type var{};làm.
ildjarn

2
@celtschk: Tôi không nói đó là điều gì đó sẽ xảy ra thường xuyên, nhưng nó ít gõ hơn và hoạt động trong nhiều ngữ cảnh hơn, vậy nhược điểm là gì?
ildjarn

2
Nguyên tắc của tôi chắc chắn không bao giờ yêu cầu khởi tạo sao chép. ; -]
ildjarn

26

Tôi khá chắc rằng sẽ không bao giờ có một hướng dẫn chung. Cách tiếp cận của tôi là sử dụng dấu ngoặc nhọn luôn nhớ rằng

  1. Các hàm tạo danh sách khởi tạo được ưu tiên hơn các hàm tạo khác
  2. Tất cả các vùng chứa thư viện chuẩn và std :: basic_string đều có các hàm tạo danh sách trình khởi tạo.
  3. Khởi tạo dấu ngoặc nhọn không cho phép chuyển đổi thu hẹp.

Vì vậy, dấu ngoặc tròn và ngoặc nhọn không thể hoán đổi cho nhau. Nhưng biết chúng khác nhau ở điểm nào cho phép tôi sử dụng khởi tạo dấu ngoặc nhọn trên vòng trong hầu hết các trường hợp (một số trường hợp mà tôi không thể hiện là lỗi trình biên dịch).


6
Dấu ngoặc nhọn có nhược điểm là tôi có thể gọi nhầm hàm tạo danh sách. Dấu ngoặc tròn không. Đó không phải là lý do để sử dụng dấu ngoặc tròn theo mặc định sao?
helami

4
@user: Về phía int i = 0;tôi không nghĩ rằng có ai sẽ sử dụng int i{0}ở đó, và nó có thể gây nhầm lẫn (cũng có thể 0là nếu loại int, vì vậy sẽ không bị thu hẹp ). Đối với mọi thứ khác, tôi sẽ làm theo lời khuyên của Juancho: thích {} hơn, hãy cẩn thận với một số trường hợp không nên làm. Lưu ý rằng không có nhiều kiểu lấy danh sách bộ khởi tạo làm đối số phương thức khởi tạo, bạn có thể mong đợi các vùng chứa và các kiểu giống như vùng chứa (tuple ...) có chúng, nhưng hầu hết mã sẽ gọi phương thức khởi tạo thích hợp.
David Rodríguez - dribeas

3
@ user1304032, tùy nếu bạn quan tâm đến việc thu hẹp. Tôi làm vậy, vì vậy tôi muốn trình biên dịch nói với tôi rằng đó int i{some floating point}là lỗi, hơn là âm thầm cắt ngắn.
juanchopanza

3
Liên quan đến "prefer {}, hãy cẩn thận với một số trường hợp không nên": Giả sử hai lớp có hàm tạo tương đương về ngữ nghĩa nhưng một lớp cũng có danh sách bộ khởi tạo. Hai hàm tạo tương đương có nên được gọi khác nhau không?
helami

3
@helami: "Giả sử hai lớp có một hàm tạo tương đương về ngữ nghĩa nhưng một lớp cũng có danh sách bộ khởi tạo. Hai hàm tạo tương đương có nên được gọi khác nhau không?" Giả sử tôi gặp phải phân tích cú pháp khó chịu nhất; điều đó có thể xảy ra trên bất kỳ hàm tạo nào cho bất kỳ trường hợp nào. Sẽ dễ dàng hơn nhiều để tránh điều này nếu bạn chỉ sử dụng {}nghĩa là "khởi tạo" trừ khi bạn hoàn toàn không thể .
Nicol Bolas

16

Bên ngoài mã chung (tức là các mẫu), bạn có thể (và tôi cũng vậy) sử dụng dấu ngoặc nhọn ở mọi nơi . Một ưu điểm là nó hoạt động ở mọi nơi, chẳng hạn ngay cả khi khởi tạo trong lớp:

struct foo {
    // Ok
    std::string a = { "foo" };

    // Also ok
    std::string b { "bar" };

    // Not possible
    std::string c("qux");

    // For completeness this is possible
    std::string d = "baz";
};

hoặc cho các đối số hàm:

void foo(std::pair<int, double*>);
foo({ 42, nullptr });
// Not possible with parentheses without spelling out the type:
foo(std::pair<int, double*>(42, nullptr));

Đối với các biến mà tôi không chú ý nhiều giữa các T t = { init };hoặc các T t { init };kiểu, tôi thấy sự khác biệt là nhỏ và tệ nhất sẽ chỉ dẫn đến một thông báo trình biên dịch hữu ích về việc sử dụng sai một hàm explicittạo.

Đối với các kiểu chấp nhận std::initializer_listmặc dù rõ ràng là đôi khi cần các hàm không phải là std::initializer_listcấu tử (ví dụ cổ điển là std::vector<int> twenty_answers(20, 42);). Khi đó không cần niềng răng cũng được.


Khi nói đến mã chung (tức là trong các mẫu), đoạn cuối cùng lẽ ra phải đưa ra một số cảnh báo. Hãy xem xét những điều sau:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T { std::forward<Args>(args)... } }; }

Sau đó, auto p = make_unique<std::vector<T>>(20, T {});tạo một vectơ có kích thước 2 nếu Tlà ví dụ int, hoặc một vectơ có kích thước 20 nếu Tstd::string. Một dấu hiệu rất rõ ràng rằng có điều gì đó rất sai đang xảy ra ở đây là không có đặc điểm nào có thể cứu bạn ở đây (ví dụ: với SFINAE): std::is_constructiblelà về mặt khởi tạo trực tiếp, trong khi chúng tôi đang sử dụng khởi tạo dấu ngoặc nhọn để định hướng thành trực tiếp- khởi tạo nếu và chỉ khi không có hàm tạo nào std::initializer_listcan thiệp. Tương tự std::is_convertiblekhông có ích gì.

Tôi đã điều tra xem liệu có thực sự có thể tự tay cuộn một đặc điểm có thể khắc phục điều đó hay không nhưng tôi không quá lạc quan về điều đó. Trong mọi trường hợp, tôi không nghĩ rằng chúng ta sẽ thiếu nhiều, tôi nghĩ rằng thực tế make_unique<T>(foo, bar)dẫn đến kết quả xây dựng tương đương với T(foo, bar)rất trực quan; đặc biệt là vì điều đó make_unique<T>({ foo, bar })khá khác nhau và chỉ có ý nghĩa nếu foobarcó cùng một loại.

Do đó, đối với mã chung, tôi chỉ sử dụng dấu ngoặc nhọn để khởi tạo giá trị (ví dụ T t {};hoặc T t = {};), điều này rất thuận tiện và tôi nghĩ là tốt hơn so với cách C ++ 03 T t = T();. Nếu không, đó là cú pháp khởi tạo trực tiếp (tức là T t(a0, a1, a2);) hoặc đôi khi là cấu trúc mặc định ( T t; stream >> t;là trường hợp duy nhất tôi sử dụng mà tôi nghĩ).

Điều đó không có nghĩa là tất cả các niềng răng đều xấu, hãy xem xét ví dụ trước với các bản sửa lỗi:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T(std::forward<Args>(args)...) }; }

Điều này vẫn sử dụng dấu ngoặc nhọn để xây dựng std::unique_ptr<T>, mặc dù kiểu thực tế phụ thuộc vào tham số mẫu T.


@interjay Một số ví dụ của tôi thực sự có thể cần phải sử dụng các loại unsigned thay vào đó, ví dụ như make_unique<T>(20u, T {})cho Tlà một trong hai unsignedhoặc std::string. Không quá chắc chắn về các chi tiết. (Lưu ý rằng tôi cũng đã nhận xét về các kỳ vọng liên quan đến khởi tạo trực tiếp so với khởi tạo dấu ngoặc nhọn liên quan đến các hàm chuyển tiếp hoàn hảo.) std::string c("qux");Chưa được chỉ định để hoạt động như một khởi tạo trong lớp để tránh sự mơ hồ với các khai báo hàm thành viên trong ngữ pháp.
Luc Danton

@interjay Tôi không đồng ý với bạn ở điểm đầu tiên, vui lòng kiểm tra 8.5.4 Khởi tạo danh sách và 13.3.1.7 Khởi tạo bằng khởi tạo danh sách. Đối với điều thứ hai, bạn cần xem xét kỹ hơn những gì tôi đã viết (liên quan đến khởi tạo trong lớp ) và / hoặc ngữ pháp C ++ (ví dụ: bộ khai báo thành viên , tham chiếu đến dấu ngoặc nhọn-hoặc-bằng-khởi tạo ).
Luc Danton

Hmm, bạn nói đúng - tôi đã thử nghiệm với GCC 4.5 trước đó, điều này dường như xác nhận những gì tôi đang nói, nhưng GCC 4.6 không đồng ý với bạn. Và tôi đã bỏ lỡ thực tế là bạn đang nói về khởi tạo trong lớp. Lời xin lỗi của tôi.
giữa
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.