Tôi không hiểu tại sao tôi lại làm điều này:
struct S {
int a;
S(int aa) : a(aa) {}
S() = default;
};
Tại sao không chỉ nói:
S() {} // instead of S() = default;
Tại sao mang lại một cú pháp mới cho điều đó?
Tôi không hiểu tại sao tôi lại làm điều này:
struct S {
int a;
S(int aa) : a(aa) {}
S() = default;
};
Tại sao không chỉ nói:
S() {} // instead of S() = default;
Tại sao mang lại một cú pháp mới cho điều đó?
Câu trả lời:
Hàm tạo mặc định mặc định được xác định cụ thể giống như hàm tạo mặc định do người dùng định nghĩa không có danh sách khởi tạo và câu lệnh ghép trống.
§12.1 / 6 [class.ctor] Một constructor mặc định được cài đặt sẵn và không được định nghĩa như xóa được mặc nhiên được định nghĩa khi nó là ODR được sử dụng để tạo ra một đối tượng của kiểu lớp của nó hoặc khi nó được mặc định một cách rõ ràng sau khi tuyên bố đầu tiên của mình. Hàm tạo mặc định được định nghĩa ngầm thực hiện tập hợp các khởi tạo của lớp sẽ được thực hiện bởi hàm tạo mặc định do người dùng viết cho lớp đó mà không có trình khởi tạo ctor (12.6.2) và câu lệnh ghép rỗng. [...]
Tuy nhiên, trong khi cả hai hàm tạo sẽ hành xử giống nhau, việc cung cấp một triển khai trống sẽ ảnh hưởng đến một số thuộc tính của lớp. Đưa ra một hàm tạo do người dùng định nghĩa, mặc dù nó không làm gì, làm cho kiểu không phải là tổng hợp và cũng không tầm thường . Nếu bạn muốn lớp của bạn là một loại tổng hợp hoặc một loại tầm thường (hoặc theo độ xuyên sáng, một loại POD), thì bạn cần phải sử dụng = default
.
§8.5.1 / 1 [dcl.init.aggr] Tập hợp là một mảng hoặc một lớp không có hàm tạo do người dùng cung cấp, [và ...]
§12.1 / 5 [class.ctor] Một hàm tạo mặc định là tầm thường nếu nó không do người dùng cung cấp và [...]
§9 / 6 [class] Một lớp tầm thường là một lớp có hàm tạo mặc định tầm thường và [...]
Để lam sang tỏ:
#include <type_traits>
struct X {
X() = default;
};
struct Y {
Y() { };
};
int main() {
static_assert(std::is_trivial<X>::value, "X should be trivial");
static_assert(std::is_pod<X>::value, "X should be POD");
static_assert(!std::is_trivial<Y>::value, "Y should not be trivial");
static_assert(!std::is_pod<Y>::value, "Y should not be POD");
}
Ngoài ra, mặc định rõ ràng một hàm tạo sẽ tạo ra nó constexpr
nếu hàm tạo ngầm định đã và cũng sẽ cung cấp cho nó cùng một đặc tả ngoại lệ mà hàm tạo ẩn sẽ có. Trong trường hợp bạn đã đưa ra, hàm tạo ngầm định sẽ không có constexpr
(vì nó sẽ để lại một thành viên dữ liệu chưa được khởi tạo) và nó cũng sẽ có một đặc tả ngoại lệ trống, do đó không có sự khác biệt. Nhưng có, trong trường hợp chung, bạn có thể chỉ định thủ công constexpr
và đặc tả ngoại lệ để khớp với hàm tạo ẩn.
Việc sử dụng = default
không mang lại sự đồng nhất, bởi vì nó cũng có thể được sử dụng với các hàm tạo và di chuyển sao chép / di chuyển. Ví dụ, một hàm tạo sao chép trống sẽ không thực hiện giống như một hàm tạo sao chép mặc định (sẽ thực hiện sao chép thành viên khôn ngoan của các thành viên của nó). Sử dụng cú pháp = default
(hoặc = delete
) thống nhất cho từng hàm thành viên đặc biệt này giúp mã của bạn dễ đọc hơn bằng cách nêu rõ ý định của bạn.
constexpr
tạo (7.1.5), thì hàm tạo mặc định được xác định ngầm định là constexpr
."
constexpr
nếu tuyên bố ngầm sẽ là, (b) nó được coi là có cùng một đặc tả ngoại lệ như thể nó đã được tuyên bố ngầm (15.4), ... "Nó không có sự khác biệt trong trường hợp cụ thể này, nhưng nói chung foo() = default;
có một lợi thế nhỏ hơn foo() {}
.
constexpr
(vì một thành viên dữ liệu không được khởi tạo lại) và đặc tả ngoại lệ của nó cho phép tất cả các ngoại lệ. Tôi sẽ làm cho nó rõ ràng hơn.
constexpr
(mà bạn đã đề cập không nên tạo ra sự khác biệt ở đây): struct S1 { int m; S1() {} S1(int m) : m(m) {} }; struct S2 { int m; S2() = default; S2(int m) : m(m) {} }; constexpr S1 s1 {}; constexpr S2 s2 {};
Chỉ s1
đưa ra một lỗi, không phải s2
. Trong cả tiếng kêu và g ++.
Tôi có một ví dụ sẽ cho thấy sự khác biệt:
#include <iostream>
using namespace std;
class A
{
public:
int x;
A(){}
};
class B
{
public:
int x;
B()=default;
};
int main()
{
int x = 5;
new(&x)A(); // Call for empty constructor, which does nothing
cout << x << endl;
new(&x)B; // Call for default constructor
cout << x << endl;
new(&x)B(); // Call for default constructor + Value initialization
cout << x << endl;
return 0;
}
Đầu ra:
5
5
0
Như chúng ta có thể thấy lệnh gọi hàm tạo A () trống không khởi tạo các thành viên, trong khi B () thực hiện nó.
n2210 cung cấp một số lý do:
Việc quản lý mặc định có một số vấn đề:
- Định nghĩa xây dựng được kết hợp; khai báo bất kỳ constructor nào ngăn chặn constructor mặc định.
- Mặc định hàm hủy không phù hợp với các lớp đa hình, đòi hỏi một định nghĩa rõ ràng.
- Một khi một mặc định bị chặn, không có cách nào để phục hồi nó.
- Việc triển khai mặc định thường hiệu quả hơn so với triển khai được chỉ định thủ công.
- Việc triển khai không mặc định là không tầm thường, điều này ảnh hưởng đến ngữ nghĩa loại, ví dụ: tạo ra loại không POD.
- Không có cách nào để cấm một chức năng thành viên đặc biệt hoặc nhà điều hành toàn cầu mà không tuyên bố một sự thay thế (không tầm thường).
type::type() = default; type::type() { x = 3; }
Trong một số trường hợp, phần thân lớp có thể thay đổi mà không yêu cầu thay đổi định nghĩa hàm thành viên vì mặc định thay đổi với khai báo của các thành viên bổ sung.
Xem Rule-of-Three trở thành Rule-of-Five với C ++ 11? :
Lưu ý rằng hàm tạo di chuyển và toán tử gán di chuyển sẽ không được tạo cho một lớp khai báo rõ ràng bất kỳ hàm thành viên đặc biệt nào khác, hàm tạo sao chép và toán tử gán gán sao chép sẽ không được tạo cho một lớp khai báo rõ ràng hàm tạo di chuyển hoặc di chuyển toán tử gán, và một lớp với hàm hủy được khai báo rõ ràng và hàm tạo sao chép được định nghĩa ngầm định hoặc toán tử gán gán sao chép được định nghĩa ngầm được coi là không dùng nữa
= default
nói chung, hơn là lý do để thực hiện = default
trên một hàm tạo so với thực hiện { }
.
{}
đã là một tính năng của ngôn ngữ trước khi sự ra đời của =default
, những lý do này làm ngầm dựa vào sự khác biệt (ví dụ như "không có phương tiện để hồi sinh [a mặc định đàn áp]" ngụ ý rằng {}
là không tương đương với mặc định ).
Đó là một vấn đề ngữ nghĩa trong một số trường hợp. Nó không rõ ràng lắm với các hàm tạo mặc định, nhưng nó trở nên rõ ràng với các hàm thành viên do trình biên dịch khác tạo ra.
Đối với hàm tạo mặc định, có thể làm cho bất kỳ hàm tạo mặc định nào có phần thân trống được coi là một ứng cử viên để trở thành hàm tạo tầm thường, giống như sử dụng =default
. Rốt cuộc, các hàm tạo mặc định trống cũ là C ++ hợp pháp .
struct S {
int a;
S() {} // legal C++
};
Việc trình biên dịch có hiểu hàm tạo này là tầm thường hay không là không liên quan trong hầu hết các trường hợp ngoài tối ưu hóa (thủ công hoặc trình biên dịch).
Tuy nhiên, nỗ lực này để coi các cơ quan chức năng trống là "mặc định" bị phá vỡ hoàn toàn cho các loại chức năng thành viên khác. Hãy xem xét các hàm tạo sao chép:
struct S {
int a;
S() {}
S(const S&) {} // legal, but semantically wrong
};
Trong trường hợp trên, hàm tạo sao chép được viết với phần thân trống bây giờ là sai . Nó không còn thực sự sao chép bất cứ điều gì. Đây là một bộ ngữ nghĩa rất khác so với ngữ nghĩa của hàm tạo sao chép mặc định. Hành vi mong muốn yêu cầu bạn viết một số mã:
struct S {
int a;
S() {}
S(const S& src) : a(src.a) {} // fixed
};
Tuy nhiên, ngay cả với trường hợp đơn giản này, nó trở thành gánh nặng cho trình biên dịch khi xác minh rằng hàm tạo sao chép giống hệt với trình tạo mà nó sẽ tự tạo hoặc để thấy rằng hàm tạo sao chép là tầm thường (tương đương với a memcpy
, về cơ bản ). Trình biên dịch sẽ phải kiểm tra từng biểu thức khởi tạo thành viên và đảm bảo nó giống hệt với biểu thức để truy cập thành viên tương ứng của nguồn và không có gì khác, đảm bảo không có thành viên nào bị bỏ mặc với cấu trúc mặc định không tầm thường, v.v. trình biên dịch sẽ sử dụng để xác minh rằng các phiên bản được tạo riêng của hàm này là tầm thường.
Hãy xem xét sau đó toán tử gán sao chép có thể nhận được cả hairier, đặc biệt là trong trường hợp không tầm thường. Đó là một tấn nồi hơi mà bạn không muốn phải viết cho nhiều lớp nhưng dù sao bạn cũng bị buộc phải có trong C ++ 03:
struct T {
std::shared_ptr<int> b;
T(); // the usual definitions
T(const T&);
T& operator=(const T& src) {
if (this != &src) // not actually needed for this simple example
b = src.b; // non-trivial operation
return *this;
};
Đó là một trường hợp đơn giản, nhưng nó đã có nhiều mã hơn bạn muốn buộc phải viết cho một loại đơn giản như vậy T
(đặc biệt là khi chúng ta đưa các hoạt động di chuyển vào hỗn hợp). Chúng ta không thể dựa vào một cơ thể trống có nghĩa là "điền vào mặc định" bởi vì cơ thể trống đã hoàn toàn hợp lệ và có ý nghĩa rõ ràng. Trong thực tế, nếu phần thân trống được sử dụng để biểu thị "điền vào mặc định" thì sẽ không có cách nào để tạo ra một hàm tạo sao chép không tương tự hoặc tương tự.
Đây lại là một vấn đề nhất quán. Cơ thể trống rỗng có nghĩa là "không làm gì" nhưng đối với những thứ như các nhà xây dựng sao chép, bạn thực sự không muốn "không làm gì" mà là "làm tất cả những việc bạn thường làm nếu không bị đàn áp". Do đó =default
. Nó là cần thiết để khắc phục các hàm thành viên do trình biên dịch bị chặn như sao chép / di chuyển các hàm tạo và toán tử gán. Sau đó, nó chỉ là "hiển nhiên" để làm cho nó hoạt động cho hàm tạo mặc định.
Có thể thật tuyệt khi tạo hàm tạo mặc định với các phần tử trống và các hàm tạo thành viên / cơ sở tầm thường cũng được coi là tầm thường giống như khi chúng =default
chỉ làm cho mã cũ tối ưu hơn trong một số trường hợp, nhưng hầu hết mã cấp thấp dựa vào tầm thường các hàm tạo mặc định để tối ưu hóa cũng dựa vào các hàm tạo sao chép tầm thường. Nếu bạn sẽ phải đi và "sửa chữa" tất cả các hàm tạo bản sao cũ của mình, thì thực sự cũng không có gì nhiều để phải sửa tất cả các hàm tạo mặc định cũ của bạn. Nó cũng rõ ràng hơn và rõ ràng hơn bằng cách sử dụng một từ rõ ràng =default
để biểu thị ý định của bạn.
Có một vài điều khác mà các hàm thành viên do trình biên dịch tạo ra sẽ làm mà bạn phải thực hiện thay đổi rõ ràng để hỗ trợ. Hỗ trợ constexpr
cho các nhà xây dựng mặc định là một ví dụ. Nó chỉ dễ sử dụng về mặt tinh thần =default
hơn là phải đánh dấu các chức năng với tất cả các từ khóa đặc biệt khác và những thứ đó được ngụ ý =default
và đó là một trong những chủ đề của C ++ 11: làm cho ngôn ngữ dễ dàng hơn. Nó vẫn còn nhiều mụn cóc và thỏa hiệp ngược lại nhưng rõ ràng đó là một bước tiến lớn từ C ++ 03 khi nói đến việc dễ sử dụng.
= default
sẽ làm a=0;
và không! Tôi đã phải bỏ nó để ủng hộ : a(0)
. Tôi vẫn còn bối rối về sự hữu ích của = default
tho, nó là về hiệu suất? nó sẽ phá vỡ ở đâu đó nếu tôi không sử dụng = default
? Tôi đã cố gắng đọc tất cả các câu trả lời ở đây mua Tôi mới biết một số công cụ c ++ và tôi gặp nhiều khó khăn để hiểu nó.
a=0
Ví dụ của bạn là do hành vi của các loại tầm thường, là một chủ đề riêng biệt (mặc dù có liên quan).
= default
và vẫn a
sẽ cấp =0
? một cách nào đó? Bạn có nghĩ rằng tôi có thể tạo một câu hỏi mới như "làm thế nào để có một hàm tạo = default
và cấp các trường sẽ được khởi tạo đúng không?", btw Tôi gặp vấn đề trong một struct
và không phải là một class
, và ứng dụng đang chạy chính xác ngay cả khi không sử dụng = default
, tôi có thể thêm một cấu trúc tối thiểu vào câu hỏi đó nếu nó là một câu hỏi hay :)
struct { int a = 0; };
Nếu sau đó bạn quyết định bạn cần một nhà xây dựng, bạn có thể mặc định nó, nhưng lưu ý rằng loại sẽ không tầm thường (điều này tốt).
Do sự phản đối std::is_pod
và thay thế của nó std::is_trivial && std::is_standard_layout
, đoạn trích từ câu trả lời của @JosephMansfield trở thành:
#include <type_traits>
struct X {
X() = default;
};
struct Y {
Y() {}
};
int main() {
static_assert(std::is_trivial_v<X>, "X should be trivial");
static_assert(std::is_standard_layout_v<X>, "X should be standard layout");
static_assert(!std::is_trivial_v<Y>, "Y should not be trivial");
static_assert(std::is_standard_layout_v<Y>, "Y should be standard layout");
}
Lưu ý rằng Y
vẫn là bố trí tiêu chuẩn.
default
không phải là một từ khóa mới, nó chỉ là một cách sử dụng mới của một từ khóa đã dành riêng.