Tôi có thể liệt kê-khởi tạo một vectơ kiểu chỉ di chuyển không?


95

Nếu tôi chuyển đoạn mã sau qua ảnh chụp nhanh GCC 4.7 của mình, nó sẽ cố gắng sao chép các unique_ptrs vào vectơ.

#include <vector>
#include <memory>

int main() {
    using move_only = std::unique_ptr<int>;
    std::vector<move_only> v { move_only(), move_only(), move_only() };
}

Rõ ràng điều đó không thể hoạt động vì std::unique_ptrkhông thể sao chép:

lỗi: sử dụng hàm đã xóa 'std :: unique_ptr <_Tp, _Dp> :: unique_ptr (const std :: unique_ptr <_Tp, _Dp> &) [with _Tp = int; _Dp = std :: default_delete; std :: unique_ptr <_Tp, _Dp> = std :: unique_ptr] '

GCC có đúng khi cố gắng sao chép các con trỏ từ danh sách trình khởi tạo không?


Visual Studio và kêu vang có hành vi tương tự
Jean-Simon Brochu

Câu trả lời:


46

Tóm tắt <initializer_list>trong 18.9 làm rõ ràng một cách hợp lý rằng các phần tử của danh sách trình khởi tạo luôn được chuyển qua tham chiếu const. Thật không may, dường như không có bất kỳ cách nào để sử dụng chuyển ngữ nghĩa trong các phần tử danh sách trình khởi tạo trong bản sửa đổi hiện tại của ngôn ngữ.

Cụ thể, chúng tôi có:

typedef const E& reference;
typedef const E& const_reference;

typedef const E* iterator;
typedef const E* const_iterator;

const E* begin() const noexcept; // first element
const E* end() const noexcept; // one past the last element

4
Hãy xem xét thành ngữ <T> được mô tả trên cpptruths ( cpptruths.blogspot.com/2013/09/… ). Ý 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. trong <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.
Sumant 24/09/13

3
@Sumant Có vẻ không quá "thành ngữ với tôi": phải không, thay vào đó, UB thuần túy? vì không chỉ trình lặp mà còn có thể là các phần tử cơ bản const, không thể bị loại bỏ trong một chương trình đã được định dạng tốt.
underscore_d

62

Chỉnh sửa: Vì @Johannes dường như không muốn đăng giải pháp tốt nhất làm câu trả lời, nên tôi sẽ làm điều đó.

#include <iterator>
#include <vector>
#include <memory>

int main(){
  using move_only = std::unique_ptr<int>;
  move_only init[] = { move_only(), move_only(), move_only() };
  std::vector<move_only> v{std::make_move_iterator(std::begin(init)),
      std::make_move_iterator(std::end(init))};
}

Các trình vòng lặp được trả về std::make_move_iteratorsẽ di chuyển phần tử trỏ đến khi được tham chiếu đến.


Câu trả lời ban đầu: Chúng tôi sẽ sử dụng một loại trợ giúp nhỏ ở đây:

#include <utility>
#include <type_traits>

template<class T>
struct rref_wrapper
{ // CAUTION - very volatile, use with care
  explicit rref_wrapper(T&& v)
    : _val(std::move(v)) {}

  explicit operator T() const{
    return T{ std::move(_val) };
  }

private:
  T&& _val;
};

// only usable on temporaries
template<class T>
typename std::enable_if<
  !std::is_lvalue_reference<T>::value,
  rref_wrapper<T>
>::type rref(T&& v){
  return rref_wrapper<T>(std::move(v));
}

// lvalue reference can go away
template<class T>
void rref(T&) = delete;

Đáng buồn thay, mã chuyển tiếp ở đây sẽ không hoạt động:

std::vector<move_only> v{ rref(move_only()), rref(move_only()), rref(move_only()) };

Vì tiêu chuẩn, vì bất kỳ lý do gì, không định nghĩa một hàm tạo bản sao chuyển đổi như thế này:

// in class initializer_list
template<class U>
initializer_list(initializer_list<U> const& other);

Được initializer_list<rref_wrapper<move_only>>tạo bởi dấu ngoặc nhọn-init-list ( {...}) sẽ không chuyển đổi thành giá initializer_list<move_only>trị vector<move_only>cần. Vì vậy, chúng tôi cần khởi tạo hai bước ở đây:

std::initializer_list<rref_wrapper<move_only>> il{ rref(move_only()),
                                                   rref(move_only()),
                                                   rref(move_only()) };
std::vector<move_only> v(il.begin(), il.end());

1
Ah ... đây là tương tự rvalue của std::ref, không? Có lẽ nó nên được gọi std::rref.
Kerrek SB

17
Bây giờ, tôi đoán điều này không nên để lại mà không được đề cập trong một bình luận :) move_only m[] = { move_only(), move_only(), move_only() }; std::vector<move_only> v(std::make_move_iterator(m), std::make_move_iterator(m + 3));.
Johannes Schaub - litb 12/12/11

1
@Johannes: Đôi khi, chính những giải pháp đơn giản lại khiến tôi lẩn tránh. Mặc dù tôi phải thừa nhận, tôi vẫn chưa bận tâm đến những điều đó move_iterator.
Xeo

2
@Johannes: Ngoài ra, tại sao đó không phải là câu trả lời? :)
Xeo

1
@JohanLundberg: Tôi coi đó là một vấn đề QoI, nhưng tôi không hiểu tại sao nó không thể làm được điều đó. VC ++ của stdlib ví dụ như các lần gửi thẻ dựa trên danh mục trình vòng lặp và sử dụng std::distancecho trình vòng lặp chuyển tiếp hoặc tốt hơn và std::move_iteratorđiều chỉnh danh mục trình vòng lặp cơ bản. Dù sao, giải pháp tốt và ngắn gọn. Có thể đăng nó như một câu trả lời?
Xeo

10

Như đã đề cập trong các câu trả lời khác, hành vi của std::initializer_listlà giữ các đối tượng theo giá trị và không cho phép di chuyển ra ngoài, vì vậy điều này là không thể. Dưới đây là một giải pháp khả thi, sử dụng một lệnh gọi hàm trong đó các trình khởi tạo được đưa ra dưới dạng các đối số khác nhau:

#include <vector>
#include <memory>

struct Foo
{
    std::unique_ptr<int> u;
    int x;
    Foo(int x = 0): x(x) {}
};

template<typename V>        // recursion-ender
void multi_emplace(std::vector<V> &vec) {}

template<typename V, typename T1, typename... Types>
void multi_emplace(std::vector<V> &vec, T1&& t1, Types&&... args)
{
    vec.emplace_back( std::move(t1) );
    multi_emplace(vec, args...);
}

int main()
{
    std::vector<Foo> foos;
    multi_emplace(foos, 1, 2, 3, 4, 5);
    multi_emplace(foos, Foo{}, Foo{});
}

Thật không may, multi_emplace(foos, {});nó không thể suy ra loại cho {}, vì vậy để các đối tượng được xây dựng mặc định, bạn phải lặp lại tên lớp. (hoặc sử dụng vector::resize)


4
Việc mở rộng gói đệ quy có thể được thay thế bằng hack toán tử dấu phẩy mảng giả, để lưu một vài dòng mã
MM

0

Sử dụng mẹo của Johannes Schaub về std::make_move_iterator()với std::experimental::make_array(), bạn có thể sử dụng hàm trợ giúp:

#include <memory>
#include <type_traits>
#include <vector>
#include <experimental/array>

struct X {};

template<class T, std::size_t N>
auto make_vector( std::array<T,N>&& a )
    -> std::vector<T>
{
    return { std::make_move_iterator(std::begin(a)), std::make_move_iterator(std::end(a)) };
}

template<class... T>
auto make_vector( T&& ... t )
    -> std::vector<typename std::common_type<T...>::type>
{
    return make_vector( std::experimental::make_array( std::forward<T>(t)... ) );
}

int main()
{
    using UX = std::unique_ptr<X>;
    const auto a  = std::experimental::make_array( UX{}, UX{}, UX{} ); // Ok
    const auto v0 = make_vector( UX{}, UX{}, UX{} );                   // Ok
    //const auto v1 = std::vector< UX >{ UX{}, UX{}, UX{} };           // !! Error !!
}

Xem nó trực tiếp trên Coliru.

Có lẽ ai đó có thể tận dụng std::make_array()thủ thuật của nó để cho phép make_vector()thực hiện công việc của mình một cách trực tiếp, nhưng tôi không biết làm thế nào (chính xác hơn, tôi đã thử những gì tôi nghĩ nên hoạt động, thất bại và tiếp tục). Trong bất kỳ trường hợp nào, trình biên dịch sẽ có thể chuyển đổi nội tuyến mảng thành vectơ, như Clang đã làm với O2 trên GodBolt.


-1

Như nó đã được chỉ ra, không thể khởi tạo vectơ kiểu chỉ di chuyển với danh sách trình khởi tạo. Giải pháp ban đầu được đề xuất bởi @Johannes hoạt động tốt, nhưng tôi có một ý tưởng khác ... Điều gì sẽ xảy ra nếu chúng ta không tạo một mảng tạm thời và sau đó di chuyển các phần tử từ đó vào vector, nhưng sử dụng vị trínew để khởi tạo mảng này đã có khối bộ nhớ của vector?

Đây là hàm của tôi để khởi tạo một vectơ của unique_ptr's bằng cách sử dụng một gói đối số:

#include <iostream>
#include <vector>
#include <make_unique.h>  /// @see http://stackoverflow.com/questions/7038357/make-unique-and-perfect-forwarding

template <typename T, typename... Items>
inline std::vector<std::unique_ptr<T>> make_vector_of_unique(Items&&... items) {
    typedef std::unique_ptr<T> value_type;

    // Allocate memory for all items
    std::vector<value_type> result(sizeof...(Items));

    // Initialize the array in place of allocated memory
    new (result.data()) value_type[sizeof...(Items)] {
        make_unique<typename std::remove_reference<Items>::type>(std::forward<Items>(items))...
    };
    return result;
}

int main(int, char**)
{
    auto testVector = make_vector_of_unique<int>(1,2,3);
    for (auto const &item : testVector) {
        std::cout << *item << std::endl;
    }
}

Đó là một ý tưởng khủng khiếp. Vị trí mới không phải là một cái búa, nó là một công cụ có độ chính xác cao. result.data()không phải là một con trỏ đến một số bộ nhớ ngẫu nhiên. Nó là một con trỏ tới một đối tượng . Hãy nghĩ về những gì sẽ xảy ra với đối tượng kém đó khi bạn đặt mới trên nó.
R. Martinho Fernandes

Ngoài ra, dạng mảng của vị trí mới không thực sự có thể sử dụng được stackoverflow.com/questions/8720425/…
R. Martinho Fernandes

@R. Martinho Fernandes: cảm ơn vì đã chỉ ra rằng vị trí mới cho mảng sẽ không hoạt động. Bây giờ tôi hiểu tại sao đó là một ý tưởng tồi.
Khởi hành
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.