Khi nào tôi nên sử dụng khấu trừ kiểu trả về tự động C ++ 14?


144

Với GCC 4.8.0 được phát hành, chúng tôi có một trình biên dịch hỗ trợ khấu trừ kiểu trả về tự động, một phần của C ++ 14. Với -std=c++1y, tôi có thể làm điều này:

auto foo() { //deduced to be int
    return 5;
}

Câu hỏi của tôi là: Khi nào tôi nên sử dụng tính năng này? Khi nào cần thiết và khi nào nó làm cho mã sạch hơn?

cảnh 1

Kịch bản đầu tiên tôi có thể nghĩ đến là bất cứ khi nào có thể. Mỗi chức năng có thể được viết theo cách này nên được. Vấn đề với điều này là nó có thể không phải lúc nào cũng làm cho mã dễ đọc hơn.

Kịch bản 2

Kịch bản tiếp theo là để tránh các loại trả lại phức tạp hơn. Như một ví dụ rất nhẹ:

template<typename T, typename U>
auto add(T t, U u) { //almost deduced as decltype(t + u): decltype(auto) would
    return t + u;
}

Tôi không tin rằng đó thực sự sẽ là một vấn đề, mặc dù tôi đoán rằng kiểu trả về rõ ràng phụ thuộc vào các tham số có thể rõ ràng hơn trong một số trường hợp.

Kịch bản 3

Tiếp theo, để ngăn chặn sự dư thừa:

auto foo() {
    std::vector<std::map<std::pair<int, double>, int>> ret;
    //fill ret in with stuff
    return ret;
}

Trong C ++ 11, đôi khi chúng ta có thể chỉ return {5, 6, 7};thay thế một vectơ, nhưng điều đó không phải lúc nào cũng hoạt động và chúng ta cần chỉ định loại trong cả tiêu đề hàm và thân hàm. Điều này hoàn toàn là dự phòng, và khấu trừ kiểu trả về tự động giúp chúng tôi tránh khỏi sự dư thừa đó.

Kịch bản 4

Cuối cùng, nó có thể được sử dụng thay cho các chức năng rất đơn giản:

auto position() {
    return pos_;
}

auto area() {
    return length_ * width_;
}

Tuy nhiên, đôi khi, chúng ta có thể nhìn vào hàm, muốn biết loại chính xác và nếu nó không được cung cấp ở đó, chúng ta phải đi đến một điểm khác trong mã, như nơi pos_được khai báo.

Phần kết luận

Với những kịch bản được đặt ra, cái nào trong số chúng thực sự chứng minh là một tình huống trong đó tính năng này hữu ích trong việc làm cho mã sạch hơn? Điều gì về các kịch bản tôi đã bỏ qua để đề cập ở đây? Tôi nên thực hiện các biện pháp phòng ngừa nào trước khi sử dụng tính năng này để nó không cắn tôi sau này? Có bất cứ điều gì mới mà tính năng này mang đến cho bảng mà không thể có nếu không có nó?

Lưu ý rằng nhiều câu hỏi có nghĩa là một trợ giúp trong việc tìm kiếm quan điểm để từ đó trả lời câu hỏi này.


18
Câu hỏi tuyệt vời! Trong khi bạn hỏi kịch bản nào làm cho mã "tốt hơn", tôi cũng tự hỏi kịch bản nào sẽ làm cho mã xấu hơn .
Drew Dormann

2
@DrewDormann, đó là điều tôi cũng đang tự hỏi. Tôi thích sử dụng các tính năng mới, nhưng biết khi nào nên sử dụng chúng và khi nào không nên rất quan trọng. Có một khoảng thời gian khi các tính năng mới phát sinh mà chúng ta cần phải tìm ra điều này, vì vậy hãy thực hiện ngay bây giờ để chúng tôi sẵn sàng khi nó chính thức xuất hiện :)
chris

2
@NicolBolas, Có lẽ, nhưng thực tế là trong một bản phát hành thực tế của trình biên dịch bây giờ sẽ đủ để mọi người bắt đầu sử dụng nó trong mã cá nhân (chắc chắn phải tránh xa các dự án vào thời điểm này). Tôi là một trong những người thích sử dụng các tính năng mới nhất có thể có trong mã của riêng tôi và trong khi tôi không biết đề xuất này diễn ra tốt như thế nào với ủy ban, tôi nhận ra thực tế rằng đó là lần đầu tiên được bao gồm trong tùy chọn mới này. một cái gì đó Nó có thể tốt hơn để lại sau này, hoặc (tôi không biết nó sẽ hoạt động tốt như thế nào) đã hồi sinh khi chúng ta biết chắc chắn nó sẽ đến.
chris

1
@NicolBolas, Nếu nó giúp, nó đã được thông qua ngay bây giờ: p
chris

1
Các câu trả lời hiện tại dường như không đề cập đến việc thay thế ->decltype(t+u)bằng suy luận tự động sẽ giết chết SFINAE.
Marc Glisse

Câu trả lời:


62

C ++ 11 đặt ra câu hỏi tương tự: khi nào nên sử dụng khấu trừ kiểu trả về trong lambdas và khi nào nên sử dụng autobiến.

Câu trả lời truyền thống cho câu hỏi trong C và C ++ 03 là "vượt qua các ranh giới câu lệnh mà chúng ta tạo ra các loại rõ ràng, trong các biểu thức chúng thường ẩn nhưng chúng ta có thể làm cho chúng rõ ràng bằng các phôi". C ++ 11 và C ++ 1y giới thiệu các công cụ khấu trừ loại để bạn có thể loại bỏ các loại ở những nơi mới.

Xin lỗi, nhưng bạn sẽ không giải quyết vấn đề này bằng cách đưa ra các quy tắc chung. Bạn cần xem xét mã cụ thể và tự quyết định xem có nên dễ dàng xác định các loại ở khắp mọi nơi hay không: mã của bạn có tốt hơn để nói, "loại của thứ này là X" hay tốt hơn cho mã của bạn để nói, "loại điều này không liên quan để hiểu phần này của mã: trình biên dịch cần biết và chúng ta có thể giải quyết nó nhưng chúng ta không cần phải nói ở đây"?

Vì "khả năng đọc" không được xác định một cách khách quan [*] và hơn nữa nó khác nhau tùy theo người đọc, bạn có trách nhiệm là tác giả / biên tập viên của một đoạn mã không thể được thỏa mãn hoàn toàn bởi một hướng dẫn kiểu. Ngay cả trong phạm vi mà một hướng dẫn phong cách chỉ định các quy tắc, những người khác nhau sẽ thích các quy tắc khác nhau và sẽ có xu hướng tìm thấy bất cứ điều gì lạ lẫm là "ít đọc hơn". Vì vậy, tính dễ đọc của một quy tắc kiểu được đề xuất cụ thể thường chỉ có thể được đánh giá trong bối cảnh của các quy tắc kiểu khác.

Tất cả các kịch bản của bạn (ngay cả lần đầu tiên) sẽ được sử dụng cho phong cách mã hóa của ai đó. Cá nhân tôi thấy thứ hai là trường hợp sử dụng hấp dẫn nhất, nhưng ngay cả như vậy tôi dự đoán rằng nó sẽ phụ thuộc vào các công cụ tài liệu của bạn. Thật không hữu ích khi thấy tài liệu rằng kiểu trả về của mẫu hàm là auto, trong khi xem nó được ghi lại như là decltype(t+u)tạo ra một giao diện được xuất bản mà bạn có thể (hy vọng) dựa vào.

[*] Thỉnh thoảng có người cố gắng thực hiện một số phép đo khách quan. Trong phạm vi nhỏ mà bất kỳ ai từng đưa ra bất kỳ kết quả có ý nghĩa thống kê và áp dụng chung nào, họ hoàn toàn bị các lập trình viên làm việc phớt lờ, ủng hộ bản năng của tác giả về những gì "có thể đọc được".


1
Điểm tốt với các kết nối đến lambdas (mặc dù điều này cho phép các cơ quan chức năng phức tạp hơn). Điều chính là nó là một tính năng mới hơn và tôi vẫn đang cố gắng cân bằng ưu và nhược điểm của từng trường hợp sử dụng. Để làm điều này, thật hữu ích khi xem lý do đằng sau lý do tại sao nó sẽ được sử dụng cho mục đích gì để tôi, bản thân tôi có thể khám phá lý do tại sao tôi thích những gì tôi làm. Tôi có thể đang xem xét lại nó, nhưng đó là tôi.
chris

1
@chris: như tôi nói, tôi nghĩ tất cả đều giống nhau. Có phải mã của bạn tốt hơn để nói, "loại của thứ này là X", hay mã của bạn tốt hơn để nói, "loại của điều này là không liên quan, trình biên dịch cần biết và có lẽ chúng ta có thể giải quyết nó nhưng chúng ta không cần phải nói điều đó ". Khi chúng ta viết 1.0 + 27Uchúng ta khẳng định cái sau, khi chúng ta viết (double)1.0 + (double)27Uchúng ta khẳng định cái trước. Đơn giản về chức năng, mức độ trùng lặp, tránh decltypetất cả có thể đóng góp vào đó nhưng không có gì sẽ được quyết định đáng tin cậy.
Steve Jessop

1
Có phải mã của bạn tốt hơn để nói, "loại của thứ này là X", hay mã của bạn tốt hơn để nói, "loại của điều này là không liên quan, trình biên dịch cần biết và có lẽ chúng ta có thể giải quyết nó nhưng chúng ta không cần phải nói điều đó ". - Câu đó chính xác dọc theo dòng những gì tôi đang tìm kiếm. Tôi sẽ cân nhắc điều đó khi tôi bắt gặp các tùy chọn sử dụng tính năng này và autonói chung.
chris

1
Tôi muốn thêm rằng các IDE có thể làm giảm bớt "vấn đề dễ đọc". Lấy Visual Studio, ví dụ: Nếu bạn di chuột qua autotừ khóa, nó sẽ hiển thị loại trả về thực tế.
andreee

1
@andreee: điều đó đúng trong giới hạn. Nếu một loại có nhiều bí danh thì việc biết loại thực tế không phải lúc nào cũng hữu ích như bạn hy vọng. Ví dụ, một kiểu trình vòng lặp có thể là int*, nhưng điều thực sự quan trọng, nếu có, đó là lý do int*bởi vì đó là những gì std::vector<int>::iterator_typevới các tùy chọn xây dựng hiện tại của bạn!
Steve Jessop

30

Nói chung, kiểu trả về hàm giúp ích rất nhiều cho việc ghi lại hàm. Người dùng sẽ biết những gì được mong đợi. Tuy nhiên, có một trường hợp mà tôi nghĩ rằng có thể tốt hơn khi loại bỏ kiểu trả về đó để tránh dư thừa. Đây là một ví dụ:

template<typename F, typename Tuple, int... I>
  auto
  apply_(F&& f, Tuple&& args, int_seq<I...>) ->
  decltype(std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...))
  {
    return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...);
  }

template<typename F, typename Tuple,
         typename Indices = make_int_seq<std::tuple_size<Tuple>::value>>
  auto
  apply(F&& f, Tuple&& args) ->
  decltype(apply_(std::forward<F>(f), std::forward<Tuple>(args), Indices()))
  {
    return apply_(std::forward<F>(f), std::forward<Tuple>(args), Indices());
  }

Ví dụ này được lấy từ giấy ủy ban chính thức N3493 . Mục đích của hàm applylà chuyển tiếp các phần tử của a std::tupleđến hàm và trả về kết quả. Các int_seqmake_int_seqchỉ là một phần của việc thực hiện, và có lẽ sẽ chỉ gây nhầm lẫn bất cứ người dùng cố gắng để hiểu những gì nó làm.

Như bạn có thể thấy, kiểu trả về không có gì khác hơn một decltypebiểu thức được trả về. Hơn nữa, apply_không được người dùng nhìn thấy, tôi không chắc về tính hữu ích của việc ghi lại kiểu trả về của nó khi nó ít nhiều giống với kiểu applycủa nó. Tôi nghĩ rằng, trong trường hợp cụ thể này, việc loại bỏ kiểu trả về làm cho hàm dễ đọc hơn. Lưu ý rằng loại trả lại này thực sự đã bị loại bỏ và được thay thế bằng decltype(auto)trong đề xuất để thêm applyvào tiêu chuẩn, N3915 (cũng lưu ý rằng câu trả lời ban đầu của tôi có trước bài báo này):

template <typename F, typename Tuple, size_t... I>
decltype(auto) apply_impl(F&& f, Tuple&& t, index_sequence<I...>) {
    return forward<F>(f)(get<I>(forward<Tuple>(t))...);
}

template <typename F, typename Tuple>
decltype(auto) apply(F&& f, Tuple&& t) {
    using Indices = make_index_sequence<tuple_size<decay_t<Tuple>>::value>;
    return apply_impl(forward<F>(f), forward<Tuple>(t), Indices{});
}

Tuy nhiên, hầu hết thời gian, tốt hơn là giữ kiểu trả về đó. Trong trường hợp cụ thể mà tôi đã mô tả ở trên, loại trả về khá khó đọc và người dùng tiềm năng sẽ không nhận được bất cứ điều gì từ việc biết nó. Một tài liệu tốt với các ví dụ sẽ hữu ích hơn nhiều.


Một điều khác chưa được đề cập: trong khi declype(t+u)cho phép sử dụng biểu thức SFINAE , decltype(auto)thì không (mặc dù có một đề xuất thay đổi hành vi này). Lấy ví dụ một foobarhàm sẽ gọi foohàm thành viên của loại nếu nó tồn tại hoặc gọi barhàm thành viên của loại nếu nó tồn tại và giả sử rằng một lớp luôn có độ chính xác foohoặc barcả hai cùng một lúc:

struct X
{
    void foo() const { std::cout << "foo\n"; }
};

struct Y
{
    void bar() const { std::cout << "bar\n"; }
};

template<typename C> 
auto foobar(const C& c) -> decltype(c.foo())
{
    return c.foo();
}

template<typename C> 
auto foobar(const C& c) -> decltype(c.bar())
{
    return c.bar();
}

Gọi foobarmột thể hiện Xsẽ hiển thị footrong khi gọi foobarmột thể hiện Ysẽ hiển thị bar. Nếu bạn sử dụng khấu trừ loại trả về tự động thay thế (có hoặc không có decltype(auto)), bạn sẽ không nhận được biểu thức SFINAE và gọi foobarmột thể hiện của một trong hai Xhoặc Ysẽ gây ra lỗi thời gian biên dịch.


8

Nó không bao giờ cần thiết. Khi nào bạn nên- bạn sẽ nhận được rất nhiều câu trả lời khác nhau về điều đó. Tôi hoàn toàn không nói cho đến khi nó thực sự là một phần được chấp nhận của tiêu chuẩn và được hỗ trợ tốt bởi phần lớn các trình biên dịch chính theo cùng một cách.

Ngoài ra, nó sẽ là một cuộc tranh luận tôn giáo. Cá nhân tôi nói rằng không bao giờ đưa vào kiểu trả về thực tế giúp mã rõ ràng hơn, bảo trì dễ dàng hơn nhiều (tôi có thể nhìn vào chữ ký của hàm và biết nó trả về gì so với việc phải đọc mã) và nó loại bỏ khả năng bạn nghĩ rằng nó sẽ trả về một loại và trình biên dịch nghĩ rằng một vấn đề khác gây ra (như đã xảy ra với mọi ngôn ngữ kịch bản mà tôi từng sử dụng). Tôi nghĩ rằng ô tô là một sai lầm lớn và nó sẽ gây ra những mệnh lệnh đau đớn hơn là giúp đỡ. Những người khác sẽ nói bạn nên sử dụng nó mọi lúc, vì nó phù hợp với triết lý lập trình của họ. Ở bất kỳ giá nào, đây là cách ra khỏi phạm vi cho trang web này.


1
Tôi đồng ý rằng những người có nền tảng khác nhau sẽ có ý kiến ​​khác nhau về vấn đề này, nhưng tôi hy vọng rằng điều này sẽ đi đến kết luận C ++ - ish. Trong (gần) mọi trường hợp, không thể lạm dụng các tính năng ngôn ngữ để cố biến nó thành một tính năng khác, chẳng hạn như sử dụng #defines để biến C ++ thành VB. Các tính năng thường có sự đồng thuận tốt trong suy nghĩ của ngôn ngữ về việc sử dụng hợp lý và những gì không, theo những gì các lập trình viên ngôn ngữ đó đã quen thuộc. Tính năng tương tự có thể có trong nhiều ngôn ngữ, nhưng mỗi ngôn ngữ có hướng dẫn riêng về việc sử dụng nó.
chris

2
Một số tính năng có sự đồng thuận tốt. Nhiều không. Tôi biết rất nhiều lập trình viên nghĩ rằng phần lớn Boost là rác không nên sử dụng. Tôi cũng biết một số người nghĩ rằng điều tuyệt vời nhất sẽ xảy ra với C ++. Trong cả hai trường hợp tôi nghĩ rằng có một số cuộc thảo luận thú vị để có về nó, nhưng nó thực sự là một ví dụ chính xác về sự đóng cửa không phải là tùy chọn mang tính xây dựng trên trang web này.
Gabe Sechan

2
Không, tôi hoàn toàn không đồng ý với bạn, và tôi nghĩ đó autolà phước lành thuần túy. Nó loại bỏ rất nhiều dư thừa. Nó chỉ đơn giản là một nỗi đau để lặp lại các loại trở lại đôi khi. Nếu bạn muốn trả lại lambda, thậm chí có thể không thể lưu trữ kết quả vào một std::function, điều này có thể phải chịu một số chi phí.
Yongwei Wu

1
Có ít nhất một tình huống trong đó tất cả nhưng hoàn toàn cần thiết. Giả sử bạn cần gọi các hàm, sau đó ghi lại kết quả trước khi trả về chúng và các hàm không nhất thiết phải có cùng kiểu trả về. Nếu bạn làm như vậy thông qua hàm ghi nhật ký lấy các hàm và đối số của chúng làm tham số, thì bạn sẽ cần khai báo kiểu trả về của hàm ghi nhật ký autođể nó luôn khớp với kiểu trả về của hàm đã truyền.
Thời gian của Justin - Phục hồi lại

1
[Về mặt kỹ thuật có một cách khác để làm điều này, nhưng về cơ bản, nó sử dụng ma thuật khuôn mẫu để thực hiện chính xác điều tương tự, và vẫn cần chức năng ghi nhật ký autocho loại trả về kéo dài.]
Justin Time - Tái lập lại

7

Không có gì để làm với sự đơn giản của chức năng (như là một bản sao hiện đã bị xóa của câu hỏi này).

Loại trả về là cố định (không sử dụng auto) hoặc phụ thuộc một cách phức tạp vào tham số mẫu (sử dụng autotrong hầu hết các trường hợp, được ghép nối decltypekhi có nhiều điểm trả về).


3

Xem xét một môi trường sản xuất thực tế: nhiều chức năng và đơn vị kiểm tra tất cả phụ thuộc lẫn nhau vào loại trả về foo(). Bây giờ giả sử rằng kiểu trả về cần thay đổi vì bất kỳ lý do gì.

Nếu kiểu trả về autoở khắp mọi nơi và người gọi đến foo()và các hàm liên quan sử dụng autokhi nhận giá trị trả về, thì những thay đổi cần thực hiện là tối thiểu. Nếu không, điều này có thể có nghĩa là hàng giờ làm việc cực kỳ tẻ nhạt và dễ bị lỗi.

Là một ví dụ trong thế giới thực, tôi được yêu cầu thay đổi một mô-đun từ sử dụng con trỏ thô ở mọi nơi sang con trỏ thông minh. Sửa các bài kiểm tra đơn vị là đau đớn hơn so với mã thực tế.

Mặc dù có nhiều cách khác có thể được xử lý, việc sử dụng các autoloại trả về có vẻ như phù hợp.


1
Trong những trường hợp đó, nó sẽ tốt hơn để viết decltype(foo())?
Oren S

Cá nhân, tôi cảm thấy typedefkhai báo s và bí danh (tức là usingkhai báo kiểu) tốt hơn trong những trường hợp này, đặc biệt khi chúng nằm trong phạm vi lớp.
MAChitgarha

3

Tôi muốn cung cấp một ví dụ trong đó tự động loại trả về là hoàn hảo:

Hãy tưởng tượng bạn muốn tạo một bí danh ngắn cho một cuộc gọi chức năng dài tiếp theo. Với tự động, bạn không cần phải quan tâm đến loại trả lại ban đầu (có thể nó sẽ thay đổi trong tương lai) và người dùng có thể nhấp vào chức năng ban đầu để có được loại trả về thực sự:

inline auto CreateEntity() { return GetContext()->GetEntityManager()->CreateEntity(); }

PS: Phụ thuộc vào câu hỏi này .


2

Đối với kịch bản 3, tôi sẽ biến kiểu trả về của chữ ký hàm với biến cục bộ được trả về. Nó sẽ làm cho nó rõ ràng hơn cho các lập trình viên khách mũ hàm trả về. Như thế này:

Kịch bản 3 Để ngăn chặn sự dư thừa:

std::vector<std::map<std::pair<int, double>, int>> foo() {
    decltype(foo()) ret;
    return ret;
}

Có, nó không có từ khóa tự động nhưng hiệu trưởng là như nhau để ngăn chặn sự dư thừa và cung cấp cho các lập trình viên không có quyền truy cập vào nguồn dễ dàng hơn.


2
IMO cách khắc phục tốt hơn cho việc này là cung cấp một tên miền cụ thể cho khái niệm về bất cứ điều gì được dự định sẽ được trình bày dưới dạng vector<map<pair<int,double>,int>và sau đó sử dụng tên đó.
davidbak
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.