C ++ 11 Khởi tạo thống nhất có phải là sự thay thế cho cú pháp kiểu cũ không?


172

Tôi hiểu rằng việc khởi tạo thống nhất của C ++ 11 giải quyết một số sự mơ hồ cú pháp trong ngôn ngữ, nhưng trong rất nhiều bài thuyết trình của Bjarne Stroustrup (đặc biệt là trong các cuộc đàm phán của GoNative 2012), các ví dụ của anh chủ yếu sử dụng cú pháp này bất cứ khi nào anh đang xây dựng các đối tượng.

Bây giờ có nên sử dụng khởi tạo thống nhất trong mọi trường hợp không? Cách tiếp cận chung nên là gì đối với tính năng mới này theo phong cách mã hóa và cách sử dụng chung? Một số lý do để không sử dụng nó là gì?

Lưu ý rằng trong đầu tôi đang nghĩ chủ yếu về xây dựng đối tượng là trường hợp sử dụng của mình, nhưng nếu có các kịch bản khác cần xem xét, vui lòng cho tôi biết.


Đây có thể là một chủ đề được thảo luận tốt hơn trên Lập trình viên. Nó dường như nghiêng về phía Chủ quan tốt.
Nicol Bolas

6
@NicolBolas: Mặt khác, câu trả lời xuất sắc của bạn có thể là một ứng cử viên rất tốt cho thẻ c ++ - faq. Tôi không nghĩ rằng chúng tôi đã có một lời giải thích cho điều này được đăng trước đó.
Matthieu M.

Câu trả lời:


233

Phong cách mã hóa cuối cùng là chủ quan, và rất khó có khả năng lợi ích hiệu suất đáng kể sẽ đến từ nó. Nhưng đây là những gì tôi muốn nói rằng bạn có được từ việc sử dụng tự do khởi tạo thống nhất:

Giảm thiểu Typenames dư thừa

Hãy xem xét những điều sau đây:

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

Tại sao tôi cần gõ vec3hai lần? Có một điểm cho điều đó? Trình biên dịch biết tốt và tốt những gì hàm trả về. Tại sao tôi không thể nói, "hãy gọi cho nhà xây dựng về những gì tôi trả lại với các giá trị này và trả lại nó?" Với khởi tạo thống nhất, tôi có thể:

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

Làm tất cả mọi việc.

Thậm chí tốt hơn là cho các đối số chức năng. Xem xét điều này:

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

DoSomething("A string.");

Điều đó hoạt động mà không cần phải gõ một kiểu chữ, bởi vì std::stringbiết cách tự xây dựng từ một const char*cách ngầm định. Thật tuyệt. Nhưng nếu chuỗi đó đến từ đâu, hãy nói RapidXML. Hoặc một chuỗi Lua. Đó là, giả sử tôi thực sự biết chiều dài của chuỗi lên phía trước. Hàm std::stringtạo có một const char*sẽ phải lấy độ dài của chuỗi nếu tôi chỉ cần vượt qua a const char*.

Có một quá tải mà có một chiều dài rõ ràng mặc dù. Nhưng để sử dụng nó, tôi phải làm điều này : DoSomething(std::string(strValue, strLen)). Tại sao có thêm tên chữ trong đó? Trình biên dịch biết loại này là gì. Cũng giống như với auto, chúng ta có thể tránh có thêm các kiểu chữ:

DoSomething({strValue, strLen});

Nó chỉ hoạt động. Không đánh máy, không ồn ào, không có gì. Trình biên dịch thực hiện công việc của nó, mã ngắn hơn và mọi người đều vui vẻ.

Cấp, có những lập luận được đưa ra rằng phiên bản đầu tiên ( DoSomething(std::string(strValue, strLen))) dễ đọc hơn. Đó là, rõ ràng những gì đang xảy ra và ai đang làm gì. Điều đó đúng, ở một mức độ nào đó; hiểu mã dựa trên khởi tạo thống nhất đòi hỏi phải xem xét nguyên mẫu hàm. Đây là cùng một lý do tại sao một số người nói rằng bạn không bao giờ nên chuyển tham số bằng tham chiếu không phải là const: để bạn có thể thấy tại trang web cuộc gọi nếu một giá trị đang được sửa đổi.

Nhưng điều tương tự có thể được nói cho auto; biết những gì bạn nhận được từ auto v = GetSomething();yêu cầu nhìn vào định nghĩa của GetSomething. Nhưng điều đó đã không ngừng autođược sử dụng với sự từ bỏ gần như liều lĩnh một khi bạn có quyền truy cập vào nó. Cá nhân, tôi nghĩ rằng nó sẽ ổn khi bạn đã quen với nó. Đặc biệt với một IDE tốt.

Không bao giờ có được phân tích khó chịu nhất

Đây là một số mã.

class Bar;

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

Pop đố: là foogì? Nếu bạn trả lời "một biến", bạn đã sai. Đây thực sự là nguyên mẫu của một hàm lấy tham số của nó là hàm trả về a Barfoogiá trị trả về của hàm là int.

Đây được gọi là "Phân tích phật ý nhất" của C ++ bởi vì nó hoàn toàn không có ý nghĩa gì với con người. Nhưng đáng buồn là các quy tắc của C ++ yêu cầu điều này: nếu nó có thể được hiểu là một nguyên mẫu hàm, thì nó sẽ như vậy. Vấn đề là Bar(); đó có thể là một trong hai điều Nó có thể là một loại có tên Bar, có nghĩa là nó đang tạo tạm thời. Hoặc nó có thể là một hàm không có tham số và trả về a Bar.

Khởi tạo thống nhất không thể được hiểu là một nguyên mẫu hàm:

class Bar;

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

Bar{}luôn luôn tạo ra một tạm thời int foo{...}luôn tạo ra một biến.

Có nhiều trường hợp bạn muốn sử dụng Typename()nhưng đơn giản là không thể vì quy tắc phân tích cú pháp của C ++. Với Typename{}, không có sự mơ hồ.

Những lý do không nên

Sức mạnh thực sự duy nhất bạn từ bỏ là thu hẹp. Bạn không thể khởi tạo một giá trị nhỏ hơn với giá trị lớn hơn với khởi tạo thống nhất.

int val{5.2};

Điều đó sẽ không được biên dịch. Bạn có thể làm điều đó với khởi tạo kiểu cũ, nhưng không khởi tạo thống nhất.

Điều này đã được thực hiện một phần để làm cho danh sách khởi tạo thực sự hoạt động. Nếu không, sẽ có rất nhiều trường hợp mơ hồ liên quan đến các loại danh sách khởi tạo.

Tất nhiên, một số người có thể lập luận rằng mã như vậy xứng đáng để không biên dịch. Cá nhân tôi tình cờ đồng ý; thu hẹp rất nguy hiểm và có thể dẫn đến hành vi khó chịu. Có lẽ tốt nhất là bắt những vấn đề đó sớm ở giai đoạn biên dịch. Ít nhất, thu hẹp cho thấy ai đó không suy nghĩ quá nhiều về mã.

Lưu ý rằng trình biên dịch nói chung sẽ cảnh báo bạn về loại điều này nếu mức cảnh báo của bạn cao. Vì vậy, thực sự, tất cả những điều này là làm cho cảnh báo thành một lỗi bắt buộc. Một số có thể nói rằng dù sao bạn cũng nên làm điều đó;)

Có một lý do khác để không:

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

Cái này làm gì Nó có thể tạo ra một vector<int>trăm mục được xây dựng mặc định. Hoặc nó có thể tạo ra vector<int>với 1 mục giá trị của ai 100. Cả hai về mặt lý thuyết đều có thể.

Trong thực tế, nó làm sau này.

Tại sao? Danh sách khởi tạo sử dụng cú pháp giống như khởi tạo thống nhất. Vì vậy, phải có một số quy tắc để giải thích những gì cần làm trong trường hợp mơ hồ. Quy tắc này khá đơn giản: nếu trình biên dịch có thể sử dụng hàm tạo danh sách khởi tạo với danh sách khởi tạo cú đúp, thì nó sẽ . Vì vector<int>có một hàm tạo danh sách khởi tạo initializer_list<int>, và {100} có thể là hợp lệ initializer_list<int>, do đó nó phải như vậy .

Để có được hàm tạo kích thước, bạn phải sử dụng ()thay vì {}.

Lưu ý rằng nếu đây là một vectorthứ không thể chuyển đổi thành số nguyên, thì điều này sẽ không xảy ra. Trình khởi tạo_list sẽ không phù hợp với trình tạo danh sách trình khởi tạo của vectorloại đó và do đó trình biên dịch sẽ được tự do chọn từ các hàm tạo khác.


11
+1 Đóng đinh nó. Tôi đang xóa câu trả lời của mình vì các địa chỉ của bạn đều có cùng một điểm chi tiết hơn nhiều.
R. Martinho Fernandes

21
Điểm cuối cùng là lý do tại sao tôi thực sự thích std::vector<int> v{100, std::reserve_tag};. Tương tự với std::resize_tag. Hiện tại phải mất hai bước để dự trữ không gian vectơ.
Xèo

6
@NicolBolas - Hai điểm: Tôi nghĩ vấn đề với phân tích vexing là foo (), không phải Bar (). Nói cách khác, nếu bạn đã làm int foo(10), bạn sẽ không gặp phải vấn đề tương tự? Thứ hai, một lý do khác để không sử dụng nó dường như là vấn đề của kỹ thuật quá mức, nhưng nếu chúng ta xây dựng tất cả các đối tượng của mình bằng cách sử dụng {}, nhưng một ngày sau đó, tôi thêm một công cụ xây dựng cho danh sách khởi tạo? Bây giờ tất cả các báo cáo xây dựng của tôi biến thành các báo cáo danh sách khởi tạo. Có vẻ rất mong manh về mặt tái cấu trúc. Bất kỳ ý kiến ​​về điều này?
void.pulum

7
@RobertDailey: "nếu bạn đã làm int foo(10), bạn sẽ không gặp phải vấn đề tương tự chứ?" Số 10 là một số nguyên và một số nguyên không bao giờ có thể là một kiểu chữ. Phân tích cú pháp xuất phát từ thực tế Bar()có thể là một kiểu chữ hoặc một giá trị tạm thời. Đó là những gì tạo ra sự mơ hồ cho trình biên dịch.
Nicol Bolas

8
unpleasant behavior- có một thuật ngữ tiêu chuẩn mới cần nhớ:>
sehe

64

Tôi sẽ không đồng ý với phần câu trả lời của Nicol Bolas Tối thiểu hóa Typenames dư thừa . Vì mã được viết một lần và đọc nhiều lần, chúng ta nên cố gắng giảm thiểu thời gian cần thiết để đọc và hiểu mã, chứ không phải lượng thời gian cần thiết để viết mã. Cố gắng chỉ để giảm thiểu việc gõ là cố gắng tối ưu hóa điều sai.

Xem mã sau đây:

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

Ai đó đọc mã ở trên lần đầu tiên có lẽ sẽ không hiểu câu lệnh trả về ngay lập tức, bởi vì khi anh ta đạt đến dòng đó, anh ta sẽ quên mất kiểu trả về. Bây giờ, anh ta sẽ phải cuộn lại chữ ký hàm hoặc sử dụng một số tính năng IDE để xem loại trả về và hiểu đầy đủ câu lệnh trả về.

Và ở đây một lần nữa, không dễ để ai đó đọc mã lần đầu tiên hiểu được những gì thực sự được xây dựng:

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

DoSomething({strValue, strLen});

Đoạn mã trên sẽ bị hỏng khi ai đó quyết định rằng DoS Something cũng sẽ hỗ trợ một số loại chuỗi khác và thêm sự quá tải này:

void DoSomething(const CoolStringType& str);

Nếu CoolStringType tình cờ có một hàm tạo có const char * và size_t (giống như std :: string hiện) thì lệnh gọi tới DoS Something ({strValue, strLen}) sẽ dẫn đến lỗi mơ hồ.

Câu trả lời của tôi cho câu hỏi thực tế:
Không, Khởi tạo thống nhất không nên được coi là thay thế cho cú pháp hàm tạo kiểu cũ.

Và lý do của tôi là thế này:
Nếu hai tuyên bố không có cùng một ý định, thì chúng không nên giống nhau. Có hai loại khái niệm khởi tạo đối tượng:
1) Lấy tất cả các mục này và đổ chúng vào đối tượng này Tôi đang khởi tạo.
2) Xây dựng đối tượng này bằng các đối số mà tôi đã cung cấp làm hướng dẫn.

Ví dụ về việc sử dụng khái niệm # 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'} };

Ví dụ về việc sử dụng khái niệm # 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);

Tôi nghĩ thật tệ khi tiêu chuẩn mới cho phép mọi người khởi tạo Cầu thang như thế này:

Stairs s {2, 4, 6};

... bởi vì điều đó làm xáo trộn ý nghĩa của nhà xây dựng. Khởi tạo như thế trông giống như khái niệm số 1, nhưng thực tế không phải vậy. Nó không đổ ba giá trị khác nhau của độ cao bước vào đối tượng, mặc dù có vẻ như nó là như vậy. Và, quan trọng hơn, nếu một triển khai thư viện của Cầu thang như trên đã được xuất bản và các lập trình viên đã sử dụng nó, và sau đó nếu người triển khai thư viện sau đó thêm trình xây dựng initizer_list vào Cầu thang, thì tất cả mã đã sử dụng Cầu thang với Khởi tạo thống nhất Cú pháp sẽ phá vỡ.

Tôi nghĩ rằng cộng đồng C ++ nên đồng ý với một quy ước chung về cách sử dụng Khởi tạo thống nhất, nghĩa là thống nhất trên tất cả các khởi tạo, hoặc, như tôi đề nghị, tách hai khái niệm khởi tạo này và do đó làm rõ ý định của lập trình viên với người đọc mật mã.


SAU:
Đây là một lý do khác khiến bạn không nên nghĩ về Khởi tạo thống nhất như là một thay thế cho cú pháp cũ và tại sao bạn không thể sử dụng ký hiệu dấu ngoặc cho tất cả các lần khởi tạo:

Nói, cú pháp ưa thích của bạn để tạo một bản sao là:

T var1;
T var2 (var1);

Bây giờ bạn nghĩ rằng bạn nên thay thế tất cả các khởi tạo bằng cú pháp cú đúp mới để bạn có thể (và mã sẽ trông) phù hợp hơn. Nhưng cú pháp sử dụng dấu ngoặc nhọn không hoạt động nếu loại T là tổng hợp:

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

48
Nếu bạn có "<rất nhiều mã ở đây>" thì mã của bạn sẽ khó hiểu bất kể cú pháp.
kevin cline

8
Ngoài IMO, nhiệm vụ của IDE là thông báo cho bạn loại nào sẽ trả về (ví dụ: thông qua di chuột). Tất nhiên, nếu bạn không sử dụng IDE, bạn sẽ tự gánh lấy mình :)
abergmeier

4
@TommiT Tôi đồng ý với một số phần của những gì bạn nói. Tuy nhiên, theo tinh thần tương tự như cuộc tranh luận khai báo loạiauto so với rõ ràng , tôi sẽ tranh luận về sự cân bằng: công cụ khởi tạo thống nhất tạo ra thời gian khá lớn trong các tình huống lập trình meta mẫu trong đó loại thường khá rõ ràng. Nó sẽ tránh lặp lại phức tạp -> decltype(....)chết tiệt của bạn cho câu thần chú, ví dụ như đối với các mẫu hàm oneline đơn giản (làm tôi khóc).
sehe

5
" Nhưng cú pháp sử dụng dấu ngoặc nhọn không hoạt động nếu loại T là tổng hợp: " Lưu ý rằng đây là lỗi được báo cáo trong tiêu chuẩn, thay vì hành vi dự kiến, có chủ ý.
Nicol Bolas

5
"Bây giờ, anh ta sẽ phải cuộn lại chữ ký hàm" nếu bạn phải cuộn, chức năng của bạn quá lớn.
Miles Rout

-3

Nếu các hàm tạo của bạn merely copy their parameterstrong các biến lớp tương ứng in exactly the same ordertrong đó chúng được khai báo bên trong lớp, thì việc sử dụng khởi tạo thống nhất cuối cùng có thể nhanh hơn (nhưng cũng có thể hoàn toàn giống nhau) so với gọi hàm tạo.

Rõ ràng, điều này không thay đổi thực tế là bạn phải luôn khai báo hàm tạo.


2
Tại sao bạn nói nó có thể nhanh hơn?
jbcoe

Điều này là không đúng. Không có yêu cầu để khai báo một hàm tạo : struct X { int i; }; int main() { X x{42}; }. Nó cũng không chính xác, việc khởi tạo thống nhất có thể nhanh hơn khởi tạo giá trị.
Tim
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.