Tại sao sử dụng các hàm bắt đầu và kết thúc không phải thành viên trong C ++ 11?


197

Mỗi container tiêu chuẩn có một beginendphương thức trả về các vòng lặp cho container đó. Tuy nhiên, C ++ 11 rõ ràng đã giới thiệu các hàm miễn phí được gọi std::beginstd::endgọi hàm này beginvà các endhàm thành viên. Vì vậy, thay vì viết

auto i = v.begin();
auto e = v.end();

bạn sẽ viết

auto i = std::begin(v);
auto e = std::end(v);

Trong bài nói chuyện của mình, Writing Modern C ++ , Herb Sutter nói rằng bạn nên luôn luôn sử dụng các chức năng miễn phí ngay bây giờ khi bạn muốn trình lặp bắt đầu hoặc kết thúc cho một container. Tuy nhiên, anh ta không đi sâu vào chi tiết lý do tại sao bạn muốn. Nhìn vào mã, nó giúp bạn tiết kiệm tất cả một ký tự. Vì vậy, theo như các container tiêu chuẩn, các chức năng miễn phí dường như hoàn toàn vô dụng. Herb Sutter chỉ ra rằng có những lợi ích cho các container không chuẩn, nhưng một lần nữa, anh không đi sâu vào chi tiết.

Vì vậy, câu hỏi đặt ra là chính xác những gì các phiên bản chức năng miễn phí std::beginstd::endlàm ngoài việc gọi các phiên bản chức năng thành viên tương ứng của chúng, và tại sao bạn muốn sử dụng chúng?


29
Đó là một ký tự ít hơn, hãy lưu những dấu chấm đó cho con của bạn: xkcd.com/297
HostileFork nói rằng đừng tin vào SE

Tôi bằng cách nào đó ghét sử dụng chúng bởi vì tôi phải lặp đi lặp lại std::mọi lúc.
Michael Chourdakis

Câu trả lời:


162

Làm thế nào để bạn gọi .begin().end()trên một mảng C?

Các hàm miễn phí cho phép lập trình chung hơn vì chúng có thể được thêm vào sau đó, trên cấu trúc dữ liệu mà bạn không thể thay đổi.


7
@JonathanMDavis: bạn có thể có các endmảng được khai báo tĩnh ( int foo[5]) bằng các thủ thuật lập trình mẫu. Một khi nó đã phân rã thành một con trỏ, bạn tất nhiên không gặp may.
Matthieu M.

33
template<typename T, size_t N> T* end(T (&a)[N]) { return a + N; }
Hugh

6
@JonathanMDavis: Như những người khác đã chỉ ra, chắc chắn có thể lấy beginendtrên một mảng C miễn là bạn chưa tự phân rã nó thành một con trỏ - @Huw đánh vần nó. Về lý do tại sao bạn muốn: hãy tưởng tượng rằng bạn đã tái cấu trúc mã đang sử dụng một mảng để sử dụng một vectơ (hoặc ngược lại, vì bất kỳ lý do gì). Nếu bạn đã sử dụng beginend, và có lẽ một số cách gõ thông minh, mã thực thi sẽ không phải thay đổi gì cả (ngoại trừ một số typedefs).
Karl Knechtel

31
@JonathanMDavis: Mảng không phải là con trỏ. Và đối với tất cả mọi người: Vì mục đích chấm dứt sự nhầm lẫn chưa từng thấy này, hãy ngừng coi (một số) con trỏ là "mảng phân rã". Không có thuật ngữ như vậy trong ngôn ngữ, và thực sự không có cách sử dụng nó. Con trỏ là con trỏ, mảng là mảng. Mảng có thể được chuyển đổi thành một con trỏ thành phần tử đầu tiên của chúng, nhưng nó vẫn chỉ là một con trỏ cũ thông thường, không có sự phân biệt với các phần tử khác. Tất nhiên, bạn không thể có được "kết thúc" của một con trỏ, trường hợp đóng.
GManNickG

5
Chà, ngoài các mảng còn có một số lượng lớn API phơi bày các khía cạnh như vùng chứa. Rõ ràng bạn không thể sửa đổi API của bên thứ 3 nhưng bạn có thể dễ dàng viết các hàm bắt đầu / kết thúc miễn phí này.
edA-qa mort-ora-y

35

Hãy xem xét trường hợp khi bạn có thư viện chứa lớp:

class SpecialArray;

nó có 2 phương thức:

int SpecialArray::arraySize();
int SpecialArray::valueAt(int);

để lặp lại các giá trị của nó, bạn cần kế thừa từ lớp này và định nghĩa begin()end()phương thức cho các trường hợp khi

auto i = v.begin();
auto e = v.end();

Nhưng nếu bạn luôn sử dụng

auto i = begin(v);
auto e = end(v);

bạn có thể làm được việc này:

template <>
SpecialArrayIterator begin(SpecialArray & arr)
{
  return SpecialArrayIterator(&arr, 0);
}

template <>
SpecialArrayIterator end(SpecialArray & arr)
{
  return SpecialArrayIterator(&arr, arr.arraySize());
}

nơi SpecialArrayIteratorlà một cái gì đó như:

class SpecialArrayIterator
{
   SpecialArrayIterator(SpecialArray * p, int i)
    :index(i), parray(p)
   {
   }
   SpecialArrayIterator operator ++();
   SpecialArrayIterator operator --();
   SpecialArrayIterator operator ++(int);
   SpecialArrayIterator operator --(int);
   int operator *()
   {
     return parray->valueAt(index);
   }
   bool operator ==(SpecialArray &);
   // etc
private:
   SpecialArray *parray;
   int index;
   // etc
};

bây giờ iecó thể được sử dụng hợp pháp để lặp và truy cập các giá trị của SpecialArray


8
Điều này không nên bao gồm các template<>dòng. Bạn đang khai báo một chức năng mới quá tải, không chuyên một mẫu.
David Stone

33

Sử dụng các hàm miễn phí beginendthêm một lớp cảm ứng. Thông thường điều đó được thực hiện để cho phép linh hoạt hơn.

Trong trường hợp này tôi có thể nghĩ ra một vài cách sử dụng.

Việc sử dụng rõ ràng nhất là cho mảng C (không phải con trỏ c).

Một cách khác là khi cố gắng sử dụng một thuật toán tiêu chuẩn trên một container không tuân thủ (tức là container bị thiếu một .begin()phương thức). Giả sử bạn không thể sửa chữa container, tùy chọn tốt nhất tiếp theo là quá tải beginchức năng. Herb đề nghị bạn luôn sử dụng beginchức năng để thúc đẩy tính đồng nhất và nhất quán trong mã của bạn. Thay vì phải nhớ phương thức hỗ trợ container nào beginvà chức năng nào cần begin.

Là một sang một bên, C ++ tiếp theo rev nên sao chép D's ký hiệu giả thành viên . Nếu a.foo(b,c,d)không được xác định nó thay vào đó cố gắng foo(a,b,c,d). Nó chỉ là một chút cú pháp để giúp chúng ta những người nghèo thích chủ đề sau đó sắp xếp động từ.


5
Các ký hiệu giả viên trông giống như C # /. Net phương pháp khuyến nông . Chúng thực sự hữu ích cho các tình huống khác nhau - như tất cả các tính năng - có thể dễ bị 'lạm dụng'.
Gareth Wilson

5
Ký hiệu thành viên giả là một lợi ích cho việc mã hóa với Intellisense; nhấn "a." hiển thị các động từ có liên quan, giải phóng sức mạnh não bộ khỏi việc ghi nhớ danh sách và giúp khám phá các chức năng API có liên quan có thể giúp ngăn chặn chức năng sao chép, mà không phải chuyển các chức năng không phải thành viên vào các lớp.
Matt Curtis

Có những đề xuất để đưa nó vào C ++, sử dụng thuật ngữ Cú pháp gọi hàm hợp nhất (UFCS).
gạch dưới

17

Để trả lời câu hỏi của bạn, các hàm miễn phí bắt đầu () và end () theo mặc định không làm gì khác hơn là gọi các hàm .begin () và .end () của thành viên chứa. Từ <iterator>, bao gồm tự động khi bạn sử dụng bất kỳ container tiêu chuẩn như <vector>, <list>vv, bạn nhận được:

template< class C > 
auto begin( C& c ) -> decltype(c.begin());
template< class C > 
auto begin( const C& c ) -> decltype(c.begin()); 

Phần thứ hai của câu hỏi của bạn là tại sao thích các chức năng miễn phí hơn nếu tất cả những gì họ làm là gọi các chức năng thành viên. Điều đó thực sự phụ thuộc vào loại đối tượng vtrong mã ví dụ của bạn. Nếu loại v là loại thùng chứa tiêu chuẩn, như vector<T> v;vậy thì không vấn đề gì nếu bạn sử dụng các hàm miễn phí hoặc thành viên, chúng cũng làm điều tương tự. Nếu đối tượng của bạn vchung chung hơn, như trong đoạn mã sau:

template <class T>
void foo(T& v) {
  auto i = v.begin();     
  auto e = v.end(); 
  for(; i != e; i++) { /* .. do something with i .. */ } 
}

Sau đó, việc sử dụng các hàm thành viên sẽ phá vỡ mã của bạn cho các mảng T = C, chuỗi C, enum, v.v. Bằng cách sử dụng các hàm không phải thành viên, bạn quảng cáo một giao diện chung hơn mà mọi người có thể dễ dàng mở rộng. Bằng cách sử dụng giao diện chức năng miễn phí:

template <class T>
void foo(T& v) {
  auto i = begin(v);     
  auto e = end(v); 
  for(; i != e; i++) { /* .. do something with i .. */ } 
}

Mã bây giờ hoạt động với mảng T = C và chuỗi C. Bây giờ viết một số lượng nhỏ mã bộ điều hợp:

enum class color { RED, GREEN, BLUE };
static color colors[]  = { color::RED, color::GREEN, color::BLUE };
color* begin(const color& c) { return begin(colors); }
color* end(const color& c)   { return end(colors); }

Chúng tôi cũng có thể nhận được mã của bạn để tương thích với các enum có thể lặp lại. Tôi nghĩ rằng điểm chính của Herb là việc sử dụng các hàm miễn phí cũng dễ như sử dụng các hàm thành viên và nó mang lại cho mã của bạn khả năng tương thích ngược với các loại chuỗi C và khả năng tương thích chuyển tiếp với các loại chuỗi không stl (và các loại stl trong tương lai!), với chi phí thấp cho các nhà phát triển khác.


Ví dụ tốt đẹp. enumMặc dù vậy, tôi sẽ không lấy một hoặc bất kỳ loại cơ bản nào khác; chúng sẽ rẻ hơn để sao chép so với chúng là gián tiếp.
gạch dưới

6

Một lợi ích của std::beginstd::endlà chúng đóng vai trò là điểm mở rộng để thực hiện giao diện chuẩn cho các lớp bên ngoài.

Nếu bạn muốn sử dụng CustomContainerlớp với hàm vòng lặp hoặc hàm mẫu dựa trên phạm vi mong đợi .begin().end()phương thức, rõ ràng bạn phải thực hiện các phương thức đó.

Nếu lớp học cung cấp các phương thức đó, đó không phải là vấn đề. Khi không, bạn phải sửa đổi nó *.

Điều này không phải lúc nào cũng khả thi, ví dụ như khi sử dụng thư viện bên ngoài, đặc biệt là nguồn thương mại và nguồn đóng.

Trong các tình huống như vậy, std::beginstd::endcó ích, vì người ta có thể cung cấp API lặp mà không cần sửa đổi chính lớp đó, mà thay vào đó là quá tải các hàm miễn phí.

Ví dụ: giả sử rằng bạn muốn triển khai count_ifhàm lấy một container thay vì một cặp trình vòng lặp. Mã như vậy có thể trông như thế này:

template<typename ContainerType, typename PredicateType>
std::size_t count_if(const ContainerType& container, PredicateType&& predicate)
{
    using std::begin;
    using std::end;

    return std::count_if(begin(container), end(container),
                         std::forward<PredicateType&&>(predicate));
}

Bây giờ, đối với bất kỳ lớp nào bạn muốn sử dụng với tùy chỉnh này count_if, bạn chỉ phải thêm hai hàm miễn phí, thay vì sửa đổi các lớp đó.

Bây giờ, C ++ có một cơ chế gọi là Tra cứu phụ thuộc đối số (ADL), giúp cho cách tiếp cận như vậy thậm chí linh hoạt hơn.

Nói tóm lại, ADL có nghĩa là, khi trình biên dịch giải quyết một hàm không đủ tiêu chuẩn (nghĩa là hàm không có không gian tên, như beginthay vì std::begin), nó cũng sẽ xem xét các hàm được khai báo trong không gian tên của các đối số của nó. Ví dụ:

namesapce some_lib
{
    // let's assume that CustomContainer stores elements sequentially,
    // and has data() and size() methods, but not begin() and end() methods:

    class CustomContainer
    {
        ...
    };
}

namespace some_lib
{    
    const Element* begin(const CustomContainer& c)
    {
        return c.data();
    }

    const Element* end(const CustomContainer& c)
    {
        return c.data() + c.size();
    }
}

// somewhere else:
CustomContainer c;
std::size_t n = count_if(c, somePredicate);

Trong trường hợp này, nó không quan trọng mà tên tiêu chuẩn là some_lib::beginsome_lib::end - kể từ khi CustomContainerđang trong some_lib::quá, trình biên dịch sẽ sử dụng những quá tải trong count_if.

Đó cũng là lý do để có using std::begin;using std::end;trong count_if. Điều này cho phép chúng tôi sử dụng không đủ tiêu chuẩn beginenddo đó cho phép ADL cho phép trình biên dịch chọn std::beginstd::endkhi không tìm thấy giải pháp thay thế nào khác.

Chúng ta có thể ăn cookie và có cookie - tức là có cách cung cấp tùy chỉnh thực hiện begin/ endtrong khi trình biên dịch có thể quay lại tiêu chuẩn.

Một số lưu ý:

  • Vì lý do tương tự, có các hàm tương tự khác: std::rbegin/ rend, std::sizestd::data.

  • Như các câu trả lời khác đề cập, std::các phiên bản có quá tải cho mảng trần. Điều đó hữu ích, nhưng chỉ đơn giản là một trường hợp đặc biệt của những gì tôi đã mô tả ở trên.

  • Sử dụng std::beginvà bạn bè là ý tưởng đặc biệt tốt khi viết mã mẫu, bởi vì điều này làm cho các mẫu đó chung chung hơn. Đối với phi mẫu, bạn cũng có thể sử dụng các phương thức, khi áp dụng.

PS Tôi biết rằng bài này đã gần 7 tuổi. Tôi đã xem qua nó vì tôi muốn trả lời một câu hỏi được đánh dấu là trùng lặp và phát hiện ra rằng không có câu trả lời nào ở đây đề cập đến ADL.


Câu trả lời hay, đặc biệt là giải thích công khai về ADL, thay vì để nó theo trí tưởng tượng như mọi người khác đã làm - ngay cả khi họ đang thể hiện nó bằng hành động!
underscore_d

5

Trong khi các hàm không phải là thành viên không cung cấp bất kỳ lợi ích nào cho các thùng chứa tiêu chuẩn, sử dụng chúng sẽ tạo ra một kiểu linh hoạt và nhất quán hơn. Nếu đôi khi bạn muốn mở rộng một lớp container không có sẵn, bạn muốn xác định tình trạng quá tải của các hàm miễn phí, thay vì thay đổi định nghĩa của lớp hiện có. Vì vậy, đối với các thùng chứa không phải là tiêu chuẩn, chúng rất hữu ích và luôn sử dụng các hàm miễn phí giúp mã của bạn linh hoạt hơn ở chỗ bạn có thể thay thế thùng chứa std bằng một thùng chứa không phải std dễ dàng hơn và loại thùng chứa bên dưới rõ ràng hơn với mã của bạn vì nó hỗ trợ nhiều loại triển khai container hơn.

Nhưng tất nhiên điều này luôn phải được cân nhắc đúng mức và quá trừu tượng cũng không tốt. Mặc dù việc sử dụng các hàm miễn phí không phải là quá trừu tượng, tuy nhiên nó vẫn phá vỡ tính tương thích với mã C ++ 03, ở độ tuổi trẻ này của C ++ 11 vẫn có thể là một vấn đề đối với bạn.


3
Trong C ++ 03, bạn chỉ có thể sử dụng boost::begin()/ end(), vì vậy không có sự không tương thích thực sự :)
Marc Mutz - mmutz

1
@ MarcMutz-mmutz Chà, tăng sự phụ thuộc không phải lúc nào cũng là một lựa chọn (và khá là quá mức nếu chỉ được sử dụng cho begin/end). Vì vậy, tôi cũng cho rằng không tương thích với C ++ 03 thuần túy. Nhưng như đã nói, đó là một sự không tương thích khá nhỏ (và ngày càng nhỏ hơn), vì C ++ 11 (ít nhất begin/endlà cụ thể) đang ngày càng được chấp nhận nhiều hơn.
Christian Rau

0

Cuối cùng, lợi ích là trong mã được khái quát hóa sao cho nó là bất khả tri. Nó có thể hoạt động trên một std::vector, một mảng hoặc một phạm vi mà không thay đổi chính mã.

Ngoài ra, các container, thậm chí các container không thuộc sở hữu có thể được trang bị thêm sao cho chúng cũng có thể được sử dụng theo cách thức sử dụng mã bằng cách sử dụng các bộ truy cập dựa trên phạm vi không phải thành viên.

Xem ở đây để biết thêm chi tiết.

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.