Tại sao không có phương thức chuyển-chuyển / chuyển-xây mặc định?


89

Tôi là một lập trình viên đơn giản. Các biến thành viên trong lớp của tôi thường bao gồm các kiểu POD và STL-container. Vì điều này, tôi hiếm khi phải viết các toán tử gán hoặc sao chép các hàm tạo, vì chúng được thực hiện theo mặc định.

Thêm vào đó, nếu tôi sử dụng std::movetrên các đối tượng không thể di chuyển, nó sử dụng toán tử gán, nghĩa std::movelà hoàn toàn an toàn.

Vì tôi là một lập trình viên đơn giản, tôi muốn tận dụng khả năng di chuyển mà không cần thêm toán tử gán / hàm khởi tạo di chuyển vào mọi lớp tôi viết, vì trình biên dịch có thể đơn giản triển khai chúng dưới dạng " this->member1_ = std::move(other.member1_);..."

Nhưng nó không (ít nhất là không có trong Visual 2010), có lý do cụ thể nào cho điều này không?

Quan trọng hơn; có cách nào để làm được việc này?

Cập nhật: Nếu bạn nhìn xuống câu trả lời của GManNickG, anh ấy cung cấp một macro tuyệt vời cho việc này. Và nếu bạn không biết, nếu bạn triển khai ngữ nghĩa di chuyển, bạn có thể loại bỏ chức năng hoán đổi thành viên.


5
Bạn có biết bạn có thể có trình biên dịch tạo ra một ctor di chuyển mặc định
aaronman

3
std :: move không thực hiện chuyển, nó chỉ chuyển từ giá trị l sang giá trị r. Việc di chuyển vẫn được thực hiện bởi hàm tạo di chuyển.
Owen Delahoy

1
Bạn đang nói về MyClass::MyClass(Myclass &&) = default;?
Sandburg

Vâng, ngày nay :)
Viktor Sehr

Câu trả lời:


76

Việc tạo ngầm các hàm tạo di chuyển và toán tử gán đã gây tranh cãi và đã có những bản sửa đổi lớn trong các bản thảo gần đây của Tiêu chuẩn C ++, vì vậy các trình biên dịch hiện có có thể sẽ hoạt động khác với việc tạo ngầm.

Để biết thêm về lịch sử của vấn đề, hãy xem danh sách bài báo WG21 năm 2010 và tìm kiếm "mov"

Đặc điểm kỹ thuật hiện tại (N3225, từ tháng 11) cho biết (N3225 12,8 / 8):

Nếu định nghĩa của một lớp Xkhông khai báo rõ ràng một phương thức khởi tạo di chuyển, một phương thức sẽ được khai báo ngầm định là mặc định nếu và chỉ khi

  • X không có hàm tạo bản sao do người dùng khai báo và

  • X không có toán tử gán bản sao do người dùng khai báo,

  • X không có toán tử gán di chuyển do người dùng khai báo,

  • X không có trình hủy do người dùng khai báo và

  • phương thức khởi tạo di chuyển sẽ không được định nghĩa ngầm định là đã xóa.

Có ngôn ngữ tương tự trong 12.8 / 22 chỉ định khi nào toán tử gán di chuyển được khai báo ngầm là mặc định. Bạn có thể tìm thấy danh sách đầy đủ các thay đổi được thực hiện để hỗ trợ đặc điểm kỹ thuật hiện tại của việc tạo ra bước di chuyển ngầm trong N3203: Thắt chặt các điều kiện để tạo ra các bước di chuyển ngầm , phần lớn dựa trên một trong những giải pháp được đề xuất bởi bài báo N3201 của Bjarne Stroustrup : Di chuyển đúng hướng .


4
Tôi đã viết một bài báo nhỏ với một số sơ đồ mô tả các mối quan hệ cho ẩn (move) constructor / chuyển nhượng ở đây: mmocny.wordpress.com/2010/12/09/implicit-move-wont-go
mmocny

Ugh nên bất cứ khi nào tôi phải xác định hàm hủy trống trong các lớp cơ sở đa hình chỉ vì lợi ích của cách xác định nó như là ảo, tôi phải xác định một cách rõ ràng các nhà xây dựng di chuyển và toán tử gán cũng :(.
someguy

@James McNellis: Đó là điều mà tôi đã thử trước đây, nhưng trình biên dịch có vẻ không thích nó. Tôi đã định đăng thông báo lỗi trong chính câu trả lời này, nhưng sau khi cố gắng tạo lại lỗi, tôi nhận ra rằng nó đề cập đến nó cannot be defaulted *in the class body*. Vì vậy, tôi đã xác định hàm hủy bên ngoài và nó đã hoạt động :). Tôi thấy nó hơi lạ, mặc dù vậy. Có ai có một lời giải thích? Trình biên dịch là gcc 4.6.1
someguy

3
Có lẽ bây giờ chúng ta có thể nhận được bản cập nhật cho câu trả lời này rằng C ++ 11 đã được phê chuẩn? Tò mò những hành vi nào đã chiến thắng.
Joseph Garvin

2
@Guy Avraham: Tôi nghĩ những gì tôi đã nói (đã 7 năm rồi) là nếu tôi có một hàm hủy do người dùng khai báo (thậm chí là một bộ ảo trống), thì sẽ không có hàm tạo di chuyển nào được khai báo ngầm như mặc định. Tôi cho rằng điều đó sẽ dẫn đến ngữ nghĩa sao chép? (Tôi đã không đụng đến C ++ trong nhiều năm.) Sau đó James McNellis nhận xét rằng nó virtual ~D() = default;sẽ hoạt động và vẫn cho phép một hàm tạo chuyển động ngầm.
someguy

13

Các hàm tạo chuyển động được tạo ngầm đã được xem xét cho tiêu chuẩn, nhưng có thể nguy hiểm. Xem phân tích của Dave Abrahams .

Tuy nhiên, cuối cùng, tiêu chuẩn đã bao gồm việc tạo ngầm định các hàm tạo di chuyển và các toán tử gán di chuyển, mặc dù với một danh sách hạn chế khá lớn:

Nếu định nghĩa của một lớp X không khai báo rõ ràng một phương thức khởi tạo di chuyển, một phương thức sẽ được khai báo ngầm định là mặc định nếu và chỉ khi
- X không có phương thức khởi tạo sao chép do người dùng khai báo,
- X không có toán tử gán bản sao do người dùng khai báo ,
- X không có toán tử gán di chuyển do người dùng khai báo,
- X không có hàm hủy do người dùng khai báo và
- hàm tạo di chuyển sẽ không được định nghĩa ngầm là đã xóa.

Đó không phải là tất cả những gì có trong câu chuyện. Một ctor có thể được khai báo, nhưng vẫn được định nghĩa là đã xóa:

Một hàm tạo sao chép / di chuyển được khai báo ngầm là một thành viên công khai nội tuyến của lớp nó. Một hàm tạo sao chép / di chuyển mặc định cho một lớp X được định nghĩa là đã xóa (8.4.3) nếu X có:

- một thành viên biến thể với một hàm tạo tương ứng không tầm thường và X là một lớp giống như union,
- một thành viên dữ liệu không tĩnh của loại lớp M (hoặc mảng của nó) không thể được sao chép / di chuyển vì độ phân giải quá tải (13.3), như được áp dụng cho hàm tạo tương ứng của M, dẫn đến sự không rõ ràng hoặc một hàm bị xóa hoặc không thể truy cập được từ hàm tạo mặc định,
- lớp cơ sở trực tiếp hoặc ảo B không thể được sao chép / di chuyển vì độ phân giải quá tải (13.3), như được áp dụng cho hàm tạo tương ứng của B. , dẫn đến sự không rõ ràng hoặc một hàm bị xóa hoặc không thể truy cập được từ hàm tạo mặc định,
- bất kỳ lớp cơ sở trực tiếp hoặc ảo hoặc thành viên dữ liệu không tĩnh nào của một loại có hàm hủy bị xóa hoặc không thể truy cập được từ hàm tạo mặc định,
- đối với hàm tạo sao chép, thành phần dữ liệu không tĩnh của kiểu tham chiếu rvalue, hoặc
- đối với hàm tạo di chuyển, thành viên dữ liệu không tĩnh hoặc lớp cơ sở trực tiếp hoặc ảo với kiểu không có hàm tạo di chuyển và không tầm thường có thể sao chép.


Dự thảo làm việc hiện tại cho phép tạo ra các động thái ngầm trong một số điều kiện nhất định và tôi nghĩ rằng nghị quyết phần lớn giải quyết các mối quan tâm của Abrahams.
James McNellis

Tôi không chắc mình đã hiểu động thái nào có thể phá vỡ trong ví dụ giữa Tweak 2 và Tweak 3. Bạn có thể giải thích không?
Matthieu M.

@Matthieu M.: cả Tweak 2 và Tweak 3 đều bị hỏng và thực sự theo những cách khá giống nhau. Trong Tweak 2, có những thành viên riêng với những bất biến có thể bị phá vỡ bởi move ctor. Trong Tweak 3, bản thân lớp không có các thành viên riêng , nhưng vì nó sử dụng kế thừa riêng, các thành viên công khai và được bảo vệ của cơ sở trở thành thành viên riêng của lớp dẫn xuất, dẫn đến cùng một vấn đề.
Jerry Coffin

1
Tôi không thực sự hiểu cách mà hàm tạo chuyển sẽ phá vỡ lớp bất biến trong Tweak2. Tôi cho rằng nó có liên quan đến thực tế là Numbersẽ được di chuyển và vectorsẽ được sao chép ... nhưng tôi không chắc: / Tôi hiểu vấn đề sẽ xảy ra Tweak3.
Matthieu M.

Liên kết bạn đưa ra dường như đã chết?
Wolf

8

(hiện tại, tôi đang làm việc trên một macro ngu ngốc ...)

Vâng, tôi cũng đã đi con đường đó. Đây là macro của bạn:

// detail/move_default.hpp
#ifndef UTILITY_DETAIL_MOVE_DEFAULT_HPP
#define UTILITY_DETAIL_MOVE_DEFAULT_HPP

#include <boost/preprocessor.hpp>

#define UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE(pR, pData, pBase) pBase(std::move(pOther))
#define UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE(pR, pData, pBase) pBase::operator=(std::move(pOther));

#define UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR(pR, pData, pMember) pMember(std::move(pOther.pMember))
#define UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT(pR, pData, pMember) pMember = std::move(pOther.pMember);

#define UTILITY_MOVE_DEFAULT_DETAIL(pT, pBases, pMembers)                                               \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE, BOOST_PP_EMPTY, pBases))                      \
        ,                                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR, BOOST_PP_EMPTY, pMembers))                         \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE, BOOST_PP_EMPTY, pBases)  \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT, BOOST_PP_EMPTY, pMembers)     \
                                                                                                        \
            return *this;                                                                               \
        }

#define UTILITY_MOVE_DEFAULT_BASES_DETAIL(pT, pBases)                                                   \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE, BOOST_PP_EMPTY, pBases))                      \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE, BOOST_PP_EMPTY, pBases)  \
                                                                                                        \
            return *this;                                                                               \
        }

#define UTILITY_MOVE_DEFAULT_MEMBERS_DETAIL(pT, pMembers)                                               \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR, BOOST_PP_EMPTY, pMembers))                         \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT, BOOST_PP_EMPTY, pMembers)     \
                                                                                                        \
            return *this;                                                                               \
        }

#endif

Các bác sĩ cho biết:

// move_default.hpp
#ifndef UTILITY_MOVE_DEFAULT_HPP
#define UTILITY_MOVE_DEFAULT_HPP

#include "utility/detail/move_default.hpp"

// move bases and members
#define UTILITY_MOVE_DEFAULT(pT, pBases, pMembers) UTILITY_MOVE_DEFAULT_DETAIL(pT, pBases, pMembers)

// base only version
#define UTILITY_MOVE_DEFAULT_BASES(pT, pBases) UTILITY_MOVE_DEFAULT_BASES_DETAIL(pT, pBases)

// member only version
#define UTILITY_MOVE_DEFAULT_MEMBERS(pT, pMembers) UTILITY_MOVE_DEFAULT_MEMBERS_DETAIL(pT, pMembers)

#endif

(Tôi đã xóa các bình luận thực, có độ dài và tài liệu.)

Bạn chỉ định cơ sở và / hoặc thành viên trong lớp của mình dưới dạng danh sách bộ xử lý trước, ví dụ:

#include "move_default.hpp"

struct foo
{
    UTILITY_MOVE_DEFAULT_MEMBERS(foo, (x)(str));

    int x;
    std::string str;
};

struct bar : foo, baz
{
    UTILITY_MOVE_DEFAULT_BASES(bar, (foo)(baz));
};

struct baz : bar
{
    UTILITY_MOVE_DEFAULT(baz, (bar), (ptr));

    void* ptr;
};

Và xuất hiện một toán tử chuyển-tạo và chuyển-gán.

(Ngoài ra, nếu ai đó biết làm thế nào tôi có thể kết hợp các chi tiết thành một macro, điều đó sẽ bị phình ra.)


Cảm ơn bạn rất nhiều, của tôi khá giống nhau, ngoại trừ tôi phải chuyển số lượng biến thành viên làm đối số (điều này thực sự hấp dẫn).
Viktor Sehr

1
@Viktor: Không sao. Nếu vẫn chưa quá muộn, tôi nghĩ bạn nên đánh dấu một trong những câu trả lời khác là được chấp nhận. Của tôi là một "nhân tiện, đây là một cách" và không phải là một câu trả lời cho câu hỏi thực sự của bạn.
GManNickG

1
Nếu tôi đang đọc macro của bạn một cách chính xác, thì ngay sau khi trình biên dịch của bạn triển khai các thành viên di chuyển mặc định, các ví dụ của bạn ở trên sẽ trở nên không thể sao chép. Việc tạo ra các thành viên sao chép ngầm bị hạn chế khi có mặt các thành viên di chuyển được khai báo rõ ràng.
Howard Hinnant

@Howard: Không sao, đó là giải pháp tạm thời cho đến lúc đó. :)
GManNickG

GMan: Macro này thêm moveconstructor \ gán nếu bạn có chức năng hoán đổi:
Viktor Sehr

4

VS2010 không làm điều đó vì họ không phải là Tiêu chuẩn tại thời điểm triển khai.

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.