Initializer_list và chuyển ngữ nghĩa


96

Tôi có được phép di chuyển các phần tử ra khỏi a std::initializer_list<T>không?

#include <initializer_list>
#include <utility>

template<typename T>
void foo(std::initializer_list<T> list)
{
    for (auto it = list.begin(); it != list.end(); ++it)
    {
        bar(std::move(*it));   // kosher?
    }
}

std::intializer_list<T>yêu cầu sự chú ý đặc biệt của trình biên dịch và không có ngữ nghĩa giá trị như các vùng chứa thông thường của thư viện chuẩn C ++, tôi muốn an toàn hơn là xin lỗi và hỏi.


Các định nghĩa ngôn ngữ cốt lõi mà các đối tượng được gọi bởi một initializer_list<T>không -const. Giống như, initializer_list<int>đề cập đến intcác đối tượng. Nhưng tôi nghĩ đó là một khiếm khuyết - mục đích là các trình biên dịch có thể cấp phát tĩnh một danh sách trong bộ nhớ chỉ đọc.
Johannes Schaub - litb

Câu trả lời:


89

Không, điều đó sẽ không hoạt động như dự định; bạn vẫn sẽ nhận được các bản sao. Tôi khá ngạc nhiên vì điều này, vì tôi đã nghĩ rằng nó initializer_listtồn tại để giữ một loạt các thời gian tạm thời cho đến khi chúng được move'd.

beginendđể initializer_listtrả về const T *, vì vậy kết quả movetrong mã của bạn là T const &&- một tham chiếu giá trị bất biến. Một biểu thức như vậy không thể được chuyển từ một cách có ý nghĩa. Nó sẽ liên kết với một tham số hàm kiểu T const &vì các giá trị liên kết với tham chiếu const lvalue, và bạn sẽ vẫn thấy ngữ nghĩa sao chép.

Có thể lý do cho điều này là do trình biên dịch có thể chọn tạo initializer_listmột hằng số được khởi tạo tĩnh, nhưng có vẻ như nó sẽ gọn gàng hơn nếu tạo kiểu của nó initializer_listhoặc const initializer_listtheo ý của trình biên dịch, vì vậy người dùng không biết có nên mong đợi một consthoặc có thể thay đổi kết quả từ beginend. Nhưng đó chỉ là cảm giác ruột gan của tôi, có lẽ có lý do chính đáng mà tôi sai.

Cập nhật: Tôi đã viết một đề xuất ISO để initializer_listhỗ trợ các loại chỉ di chuyển. Nó chỉ là bản nháp đầu tiên và nó chưa được triển khai ở bất kỳ đâu, nhưng bạn có thể xem nó để phân tích thêm về vấn đề.


11
Trong trường hợp không rõ ràng, nó vẫn có nghĩa là sử dụng std::movean toàn, nếu không hiệu quả. (Ngăn cản các nhà T const&&xây dựng chuyển động.)
Luc Danton

Tôi không nghĩ rằng bạn có thể đưa ra toàn bộ tranh luận const std::initializer_list<T>hoặc chỉ std::initializer_list<T>theo một cách không gây bất ngờ thường xuyên. Hãy xem xét rằng mỗi đối số trong initializer_listcó thể là consthoặc không và điều đó được biết trong ngữ cảnh của trình gọi, nhưng trình biên dịch chỉ phải tạo ra một phiên bản mã trong ngữ cảnh của callee (nghĩa là bên trong foonó không biết gì về các đối số mà người gọi đang chuyển đến)
David Rodríguez - dribeas

1
@David: Điểm tốt, nhưng nó vẫn hữu ích nếu có std::initializer_list &&quá tải làm gì đó, ngay cả khi quá tải không tham chiếu cũng được yêu cầu. Tôi cho rằng nó sẽ còn khó hiểu hơn tình hình hiện tại vốn đã tồi tệ.
Potatoswatter

1
@JBJansen Nó không thể bị tấn công xung quanh. Tôi không biết chính xác mã đó phải thực hiện wrt initializer_list là gì, nhưng với tư cách là người dùng, bạn không có quyền cần thiết để di chuyển khỏi nó. Mã an toàn sẽ không làm như vậy.
Potatoswatter

1
@Potatoswatter, nhận xét muộn, nhưng trạng thái của đề xuất là gì. Có bất kỳ cơ hội từ xa nào nó có thể biến nó thành C ++ 20 không?
WhiZTiM 19/07/17

20
bar(std::move(*it));   // kosher?

Không theo cách mà bạn dự định. Bạn không thể di chuyển một constđối tượng. Và std::initializer_listchỉ cung cấpconst quyền truy cập vào các phần tử của nó. Vì vậy, loại của itconst T *.

Nỗ lực gọi của std::move(*it)bạn sẽ chỉ dẫn đến giá trị l. IE: một bản sao.

std::initializer_listtham chiếu đến bộ nhớ tĩnh . Đó là những gì lớp học dành cho. Bạn không thể di chuyển từ bộ nhớ tĩnh, bởi vì chuyển động có nghĩa là thay đổi nó. Bạn chỉ có thể sao chép từ nó.


Một const xvalue vẫn là một xvalue và initializer_listtham chiếu đến ngăn xếp nếu điều đó là cần thiết. (Nếu các nội dung không phải là hằng số, nó vẫn thread-safe.)
Potatoswatter

5
@Potatoswatter: Bạn không thể di chuyển từ một đối tượng không đổi. Bản initializer_listthân đối tượng có thể là một xvalue, nhưng nội dung của nó (mảng giá trị thực mà nó trỏ tới) là constbởi vì những nội dung đó có thể là giá trị tĩnh. Bạn chỉ đơn giản là không thể di chuyển từ nội dung của một initializer_list.
Nicol Bolas

Xem câu trả lời của tôi và cuộc thảo luận về nó. Anh ta đã di chuyển trình lặp được tham chiếu đến, tạo ra một giá consttrị x. movecó thể vô nghĩa, nhưng hợp pháp và thậm chí có thể khai báo một tham số chỉ chấp nhận điều đó. Nếu việc di chuyển một loại cụ thể là không chọn, nó thậm chí có thể hoạt động chính xác.
Potatoswatter

1
@Potatoswatter: Chuẩn C ++ 11 sử dụng rất nhiều ngôn ngữ để đảm bảo rằng các đối tượng không tạm thời không thực sự được di chuyển trừ khi bạn sử dụng std::move. Điều này đảm bảo rằng bạn có thể biết từ việc kiểm tra khi một hoạt động di chuyển xảy ra, vì nó ảnh hưởng đến cả nguồn và đích (bạn không muốn nó xảy ra ngầm đối với các đối tượng được đặt tên). Do đó, nếu bạn sử dụng std::moveở một nơi mà hoạt động di chuyển không xảy ra (và không có chuyển động thực sự nào sẽ xảy ra nếu bạn có giá consttrị xvalue), thì mã sẽ bị sai lệch. Tôi nghĩ rằng đó là một sai lầm std::movekhi có thể gọi được trên một constđối tượng.
Nicol Bolas,

1
Có thể, nhưng tôi vẫn sẽ lấy ít ngoại lệ hơn cho các quy tắc về khả năng gây hiểu nhầm mã. Dù sao, đó chính xác là lý do tại sao tôi trả lời "không" mặc dù nó hợp pháp và kết quả là một xvalue ngay cả khi nó sẽ chỉ ràng buộc dưới dạng const lvalue. Thành thật mà nói, tôi đã có một cuộc tán tỉnh ngắn const &&trong một lớp thu thập rác với các con trỏ được quản lý, nơi mọi thứ liên quan đều có thể thay đổi và việc di chuyển đã di chuyển quản lý con trỏ nhưng không ảnh hưởng đến giá trị chứa. Luôn có những trường hợp hóc búa: v).
Potatoswatter

2

Điều này sẽ không hoạt động như đã nêu, bởi vì list.begin()có loại const T *và không có cách nào bạn có thể di chuyển từ một đối tượng không đổi. Các nhà thiết kế ngôn ngữ có lẽ đã làm như vậy để cho phép danh sách trình khởi tạo chứa các hằng số chuỗi chẳng hạn, từ đó nó sẽ không thích hợp để di chuyển.

Tuy nhiên, nếu bạn đang ở trong tình huống mà bạn biết rằng danh sách trình khởi tạo có chứa các biểu thức rvalue (hoặc bạn muốn buộc người dùng viết các biểu thức đó) thì có một mẹo sẽ làm cho nó hoạt động (Tôi đã lấy cảm hứng từ câu trả lời của Sumant cho này, nhưng giải pháp là cách đơn giản hơn một). Bạn cần các phần tử được lưu trữ trong danh sách trình khởi tạo không phải là Tcác giá trị, mà là các giá trị đóng gói T&&. Sau đó, ngay cả khi bản thân các giá trị đó constđủ điều kiện, chúng vẫn có thể truy xuất giá trị có thể sửa đổi.

template<typename T>
  class rref_capture
{
  T* ptr;
public:
  rref_capture(T&& x) : ptr(&x) {}
  operator T&& () const { return std::move(*ptr); } // restitute rvalue ref
};

Bây giờ thay vì khai báo một initializer_list<T>đối số, bạn khai báo một initializer_list<rref_capture<T> >đối số. Đây là một ví dụ cụ thể, liên quan đến một vectơ của các std::unique_ptr<int>con trỏ thông minh, mà chỉ các ngữ nghĩa chuyển động được xác định (vì vậy bản thân các đối tượng này không bao giờ có thể được lưu trữ trong danh sách trình khởi tạo); nhưng danh sách trình khởi tạo bên dưới biên dịch mà không có vấn đề gì.

#include <memory>
#include <initializer_list>
class uptr_vec
{
  typedef std::unique_ptr<int> uptr; // move only type
  std::vector<uptr> data;
public:
  uptr_vec(uptr_vec&& v) : data(std::move(v.data)) {}
  uptr_vec(std::initializer_list<rref_capture<uptr> > l)
    : data(l.begin(),l.end())
  {}
  uptr_vec& operator=(const uptr_vec&) = delete;
  int operator[] (size_t index) const { return *data[index]; }
};

int main()
{
  std::unique_ptr<int> a(new int(3)), b(new int(1)),c(new int(4));
  uptr_vec v { std::move(a), std::move(b), std::move(c) };
  std::cout << v[0] << "," << v[1] << "," << v[2] << std::endl;
}

Một câu hỏi cần có câu trả lời: nếu các phần tử của danh sách bộ khởi tạo phải là giá trị thực (trong ví dụ chúng là giá trị x), thì ngôn ngữ có đảm bảo rằng thời gian tồn tại của các dấu tạm thời tương ứng kéo dài đến thời điểm chúng được sử dụng không? Thành thật mà nói, tôi không nghĩ rằng phần 8.5 có liên quan của tiêu chuẩn đề cập đến vấn đề này. Tuy nhiên, đọc 1,9: 10, có vẻ như biểu thức đầy đủ có liên quan trong mọi trường hợp đều bao gồm việc sử dụng danh sách trình khởi tạo, vì vậy tôi nghĩ rằng không có nguy cơ bị treo các tham chiếu rvalue.


Hằng số chuỗi? Như "Hello world"thế nào? Nếu bạn di chuyển từ chúng, bạn chỉ cần sao chép một con trỏ (hoặc liên kết một tham chiếu).
dyp

1
"Một câu hỏi cần một câu trả lời" Các trình khởi tạo bên trong {..}được liên kết với các tham chiếu trong tham số hàm của rref_capture. Điều này không kéo dài thời gian tồn tại của chúng, chúng vẫn bị phá hủy ở phần cuối của biểu thức đầy đủ mà chúng đã được tạo ra.
dyp

Theo nhận xét của TC từ một câu trả lời khác: Nếu bạn có nhiều quá tải của hàm tạo, hãy bọc std::initializer_list<rref_capture<T>>một số đặc điểm chuyển đổi mà bạn chọn - giả sử std::decay_t- để chặn việc suy diễn không mong muốn.
Phục hồi Monica

2

Tôi nghĩ rằng nó có thể mang tính hướng dẫn để đưa ra một điểm khởi đầu hợp lý cho một giải pháp thay thế.

Nhận xét nội dòng.

#include <memory>
#include <vector>
#include <array>
#include <type_traits>
#include <algorithm>
#include <iterator>

template<class Array> struct maker;

// a maker which makes a std::vector
template<class T, class A>
struct maker<std::vector<T, A>>
{
  using result_type = std::vector<T, A>;

  template<class...Ts>
  auto operator()(Ts&&...ts) const -> result_type
  {
    result_type result;
    result.reserve(sizeof...(Ts));
    using expand = int[];
    void(expand {
      0,
      (result.push_back(std::forward<Ts>(ts)),0)...
    });

    return result;
  }
};

// a maker which makes std::array
template<class T, std::size_t N>
struct maker<std::array<T, N>>
{
  using result_type = std::array<T, N>;

  template<class...Ts>
  auto operator()(Ts&&...ts) const
  {
    return result_type { std::forward<Ts>(ts)... };
  }

};

//
// delegation function which selects the correct maker
//
template<class Array, class...Ts>
auto make(Ts&&...ts)
{
  auto m = maker<Array>();
  return m(std::forward<Ts>(ts)...);
}

// vectors and arrays of non-copyable types
using vt = std::vector<std::unique_ptr<int>>;
using at = std::array<std::unique_ptr<int>,2>;


int main(){
    // build an array, using make<> for consistency
    auto a = make<at>(std::make_unique<int>(10), std::make_unique<int>(20));

    // build a vector, using make<> because an initializer_list requires a copyable type  
    auto v = make<vt>(std::make_unique<int>(10), std::make_unique<int>(20));
}

Câu hỏi đặt ra là liệu một initializer_listcó thể được chuyển từ đâu, không phải liệu có ai có cách giải quyết hay không. Bên cạnh đó, điểm hấp dẫn chính initializer_listlà nó chỉ được tạo mẫu trên loại phần tử, không phải số lượng phần tử, và do đó không yêu cầu người nhận cũng phải được tạo mẫu - và điều này hoàn toàn mất đi điều đó.
underscore_d

1
@underscore_d bạn hoàn toàn đúng. Tôi quan điểm rằng chia sẻ kiến ​​thức liên quan đến câu hỏi tự nó là một điều tốt. Trong trường hợp này, có lẽ nó đã giúp OP và có lẽ nó không - ông ấy đã không phản hồi. Tuy nhiên, OP và những người khác thường hoan nghênh các tài liệu bổ sung liên quan đến câu hỏi.
Richard Hodges

Chắc chắn, nó thực sự có thể giúp ích cho những độc giả muốn một cái gì đó tương tự initializer_listnhưng không phải chịu tất cả các ràng buộc khiến nó hữu ích. :)
underscore_d

@underscore_d tôi đã bỏ qua những ràng buộc nào?
Richard Hodges

Tất cả ý tôi là initializer_list(thông qua phép biên dịch) tránh phải tạo khuôn mẫu các hàm trên số lượng phần tử, một thứ vốn dĩ được yêu cầu bởi các lựa chọn thay thế dựa trên mảng và / hoặc các hàm đa dạng, do đó hạn chế phạm vi các trường hợp có thể sử dụng phần tử sau. Theo hiểu biết của tôi, đây chính xác là một trong những lý do chính để có initializer_list, vì vậy nó có vẻ đáng nói.
underscore_d

0

Nó dường như không được phép trong tiêu chuẩn hiện tại như đã được trả lời . Đây là một giải pháp khác để đạt được điều gì đó tương tự, bằng cách xác định chức năng là biến thể thay vì lấy danh sách trình khởi tạo.

#include <vector>
#include <utility>

// begin helper functions

template <typename T>
void add_to_vector(std::vector<T>* vec) {}

template <typename T, typename... Args>
void add_to_vector(std::vector<T>* vec, T&& car, Args&&... cdr) {
  vec->push_back(std::forward<T>(car));
  add_to_vector(vec, std::forward<Args>(cdr)...);
}

template <typename T, typename... Args>
std::vector<T> make_vector(Args&&... args) {
  std::vector<T> result;
  add_to_vector(&result, std::forward<Args>(args)...);
  return result;
}

// end helper functions

struct S {
  S(int) {}
  S(S&&) {}
};

void bar(S&& s) {}

template <typename T, typename... Args>
void foo(Args&&... args) {
  std::vector<T> args_vec = make_vector<T>(std::forward<Args>(args)...);
  for (auto& arg : args_vec) {
    bar(std::move(arg));
  }
}

int main() {
  foo<S>(S(1), S(2), S(3));
  return 0;
}

Các mẫu đa dạng có thể xử lý các tham chiếu giá trị r một cách thích hợp, không giống như Initializer_list.

Trong mã ví dụ này, tôi đã sử dụng một tập hợp các hàm trợ giúp nhỏ để chuyển đổi các đối số khác nhau thành một vectơ, để làm cho nó tương tự như mã gốc. Nhưng tất nhiên thay vào đó bạn có thể viết một hàm đệ quy với các mẫu biến thể.


Câu hỏi đặt ra là liệu một initializer_listcó thể được chuyển từ đâu, không phải liệu có ai có cách giải quyết hay không. Bên cạnh đó, điểm hấp dẫn chính initializer_listlà nó chỉ được tạo mẫu trên loại phần tử, không phải số lượng phần tử, và do đó không yêu cầu người nhận cũng phải được tạo mẫu - và điều này hoàn toàn mất đi điều đó.
underscore_d

0

Tôi có một triển khai đơn giản hơn nhiều sử dụng một lớp wrapper hoạt động như một thẻ để đánh dấu ý định di chuyển các phần tử. Đây là chi phí thời gian biên dịch.

Lớp wrapper được thiết kế để sử dụng theo cách std::moveđược sử dụng, chỉ cần thay thế std::movebằngmove_wrapper , nhưng điều này yêu cầu C ++ 17. Đối với các thông số kỹ thuật cũ hơn, bạn có thể sử dụng phương pháp trình tạo bổ sung.

Bạn sẽ cần viết các phương thức / hàm tạo của trình xây dựng chấp nhận các lớp trình bao bọc bên trong initializer_list và di chuyển các phần tử cho phù hợp.

Nếu bạn cần một số phần tử được sao chép thay vì được di chuyển, hãy tạo một bản sao trước khi chuyển nó đến initializer_list.

Mã phải được tự tài liệu hóa.

#include <iostream>
#include <vector>
#include <initializer_list>

using namespace std;

template <typename T>
struct move_wrapper {
    T && t;

    move_wrapper(T && t) : t(move(t)) { // since it's just a wrapper for rvalues
    }

    explicit move_wrapper(T & t) : t(move(t)) { // acts as std::move
    }
};

struct Foo {
    int x;

    Foo(int x) : x(x) {
        cout << "Foo(" << x << ")\n";
    }

    Foo(Foo const & other) : x(other.x) {
        cout << "copy Foo(" << x << ")\n";
    }

    Foo(Foo && other) : x(other.x) {
        cout << "move Foo(" << x << ")\n";
    }
};

template <typename T>
struct Vec {
    vector<T> v;

    Vec(initializer_list<T> il) : v(il) {
    }

    Vec(initializer_list<move_wrapper<T>> il) {
        v.reserve(il.size());
        for (move_wrapper<T> const & w : il) {
            v.emplace_back(move(w.t));
        }
    }
};

int main() {
    Foo x{1}; // Foo(1)
    Foo y{2}; // Foo(2)

    Vec<Foo> v{Foo{3}, move_wrapper(x), Foo{y}}; // I want y to be copied
    // Foo(3)
    // copy Foo(2)
    // move Foo(3)
    // move Foo(1)
    // move Foo(2)
}

0

Thay vì sử dụng a std::initializer_list<T>, bạn có thể khai báo đối số của mình dưới dạng tham chiếu giá trị mảng:

template <typename T>
void bar(T &&value);

template <typename T, size_t N>
void foo(T (&&list)[N] ) {
   std::for_each(std::make_move_iterator(std::begin(list)),
                 std::make_move_iterator(std::end(list)),
                 &bar);
}

void baz() {
   foo({std::make_unique<int>(0), std::make_unique<int>(1)});
}

Xem ví dụ bằng cách sử dụng std::unique_ptr<int>: https://gcc.godbolt.org/z/2uNxv6


-1

Hãy xem xét in<T>thành ngữ được mô tả trên cpptruths . Ý tưởng là xác định lvalue / rvalue tại thời điểm chạy và sau đó gọi di chuyển hoặc sao chép-xây dựng. in<T>sẽ phát hiện rvalue / lvalue mặc dù giao diện tiêu chuẩn được cung cấp bởi initializer_list là tham chiếu const.


4
Tại sao bạn lại muốn xác định danh mục giá trị trong thời gian chạy khi trình biên dịch đã biết nó?
fredoverflow

1
Vui lòng đọc blog và để lại nhận xét cho tôi nếu bạn không đồng ý hoặc có giải pháp thay thế tốt hơn. Ngay cả khi trình biên dịch biết danh mục giá trị, thì initializer_list không bảo toàn nó vì nó chỉ có các trình vòng lặp const. Vì vậy, bạn cần "nắm bắt" danh mục giá trị khi bạn xây dựng danh sách khởi tạo và chuyển nó qua để hàm có thể sử dụng nó theo ý muốn.
Sumant

5
Câu trả lời này về cơ bản là vô dụng nếu không theo liên kết và câu trả lời SO sẽ hữu ích nếu không theo liên kết.
Yakk - Adam Nevraumont

1
@Sumant [sao chép bình luận của tôi từ một bài đăng giống hệt ở nơi khác] Liệu mớ hỗn độn ồn ào đó có thực sự cung cấp bất kỳ lợi ích nào có thể đo lường được đối với hiệu suất hoặc việc sử dụng bộ nhớ không, và nếu có, một lượng lợi ích đủ lớn như vậy để bù đắp đủ mức độ khủng khiếp của nó và thực tế là nó mất khoảng một giờ để tìm ra những gì nó đang cố gắng làm? Tôi hơi nghi ngờ điều đó.
underscore_d
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.