Đã bao giờ có những thay đổi hành vi âm thầm trong C ++ với các phiên bản tiêu chuẩn mới chưa?


104

(Tôi đang tìm một hoặc hai ví dụ để chứng minh quan điểm, không phải một danh sách.)

Đã bao giờ có trường hợp thay đổi trong tiêu chuẩn C ++ (ví dụ: từ 98 thành 11, 11 thành 14, v.v.) đã thay đổi hành vi của mã người dùng hiện có, được định dạng tốt, có hành vi xác định - một cách âm thầm chưa? tức là không có cảnh báo hoặc lỗi khi biên dịch với phiên bản tiêu chuẩn mới hơn?

Ghi chú:

  • Tôi đang hỏi về hành vi bắt buộc theo tiêu chuẩn, không phải về lựa chọn tác giả trình thực hiện / trình biên dịch.
  • Mã càng ít nội dung càng tốt (như một câu trả lời cho câu hỏi này).
  • Ý tôi không phải là mã có phát hiện phiên bản chẳng hạn như #if __cplusplus >= 201103L.
  • Các câu trả lời liên quan đến mô hình bộ nhớ là tốt.

Nhận xét không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
Samuel Liew

3
Tôi không hiểu tại sao câu hỏi này được đóng lại. " Đã bao giờ có những thay đổi hành vi thầm lặng trong C ++ với các phiên bản tiêu chuẩn mới chưa? " Có vẻ hoàn toàn tập trung và phần nội dung câu hỏi dường như không khác xa điều đó.
Ted Lyngmo

Trong suy nghĩ của tôi, sự thay đổi lớn nhất trong âm thầm là định nghĩa lại auto. Trước C ++ 11, auto x = ...;khai báo một int. Sau đó, nó tuyên bố bất cứ điều gì ...là.
Raymond Chen

@RaymondChen: Thay đổi này chỉ im lặng nếu bạn đang định nghĩa rõ ràng các int, nhưng nói rõ ràng các autobiến là -type. Tôi nghĩ rằng bạn có thể dựa trên một mặt số lượng người trên thế giới sẽ viết loại mã đó, ngoại trừ các cuộc thi mã C rối
rắm

Đúng, đó là lý do tại sao họ chọn nó. Nhưng đó là một sự thay đổi lớn về ngữ nghĩa.
Raymond Chen

Câu trả lời:


113

Kiểu trả về string::datathay đổi từ const char*thành char*trong C ++ 17. Điều đó chắc chắn có thể tạo ra sự khác biệt

void func(char* data)
{
    cout << data << " is not const\n";
}

void func(const char* data)
{
    cout << data << " is const\n";
}

int main()
{
    string s = "xyz";
    func(s.data());
}

Có một chút giả thiết nhưng chương trình hợp pháp này sẽ thay đổi đầu ra của nó từ C ++ 14 thành C ++ 17.


7
Ồ, tôi thậm chí còn không nhận ra đó là std::stringnhững thay đổi đối với C ++ 17. Nếu bất cứ điều gì, tôi sẽ nghĩ rằng các thay đổi C ++ 11 có thể đã gây ra thay đổi hành vi im lặng bằng cách nào đó. +1.
einpoklum

9
Có chứa hay không, điều này chứng tỏ một sự thay đổi đối với mã được định hình khá tốt.
David C. Rankin

Ngoài ra, thay đổi dựa trên các trường hợp sử dụng vui nhộn nhưng hợp pháp khi bạn thay đổi nội dung của std :: string tại chỗ, có thể thông qua các hàm kế thừa hoạt động trên char *. Điều đó hoàn toàn hợp pháp bây giờ: như với một vectơ, có một sự đảm bảo rằng có một mảng bên dưới, liền kề mà bạn có thể thao tác (bạn luôn có thể thông qua các tham chiếu được trả về; bây giờ nó được làm tự nhiên và rõ ràng hơn). Các trường hợp sử dụng có thể xảy ra là các tập dữ liệu có thể chỉnh sửa, có độ dài cố định (ví dụ: thông báo của một số loại), nếu dựa trên std :: container, giữ lại các dịch vụ của STL như quản lý thời gian sống, khả năng sao chép, v.v.
Peter - Phục hồi Monica

81

Câu trả lời cho câu hỏi này cho thấy cách khởi tạo một vectơ bằng cách sử dụng một size_typegiá trị duy nhất có thể dẫn đến hành vi khác nhau giữa C ++ 03 và C ++ 11.

std::vector<Something> s(10);

C ++ 03 mặc định-xây dựng một đối tượng tạm thời của loại phần tử Somethingvà sao chép-xây dựng từng phần tử trong vectơ từ tạm thời đó.

C ++ 11 mặc định-xây dựng từng phần tử trong vector.

Trong nhiều (hầu hết?) Trường hợp này dẫn đến trạng thái cuối cùng tương đương, nhưng không có lý do gì chúng phải làm như vậy. Nó phụ thuộc vào việc triển khai các hàm tạo Somethingmặc định / sao chép.

Xem ví dụ giả tạo này :

class Something {
private:
    static int counter;

public:
    Something() : v(counter++) {
        std::cout << "default " << v << '\n';
    }

    Something(Something const & other) : v(counter++) {
        std::cout << "copy " << other.v << " to " << v << '\n';
    }

    ~Something() {
        std::cout << "dtor " << v << '\n';
    }

private:
    int v;
};

int Something::counter = 0;

C ++ 03 sẽ mặc định-xây dựng một Somethingvới v == 0sau đó sao chép-xây dựng thêm mười từ cái đó. Ở cuối, vectơ chứa mười đối tượng có vgiá trị từ 1 đến 10, bao gồm cả.

C ++ 11 sẽ xây dựng mặc định từng phần tử. Không có bản sao nào được thực hiện. Ở cuối, vectơ chứa mười đối tượng có vgiá trị từ 0 đến 9, bao gồm cả.


@einpoklum Tuy nhiên, tôi đã thêm một ví dụ giả định. :)
cdhowie

3
Tôi không nghĩ rằng nó được tạo ra. Các hàm tạo khác nhau thường hoạt động khác nhau như cấp phát bộ nhớ. Bạn chỉ cần thay thế một hiệu ứng phụ bằng một (I / O) khác.
einpoklum

17
@cdhowie Không giống gì cả. Gần đây tôi đang làm việc trên một lớp UUID. Hàm tạo mặc định đã tạo một UUID ngẫu nhiên. Tôi không biết về khả năng này, tôi chỉ giả định hành vi C ++ 11.
john

5
Một ví dụ thực tế được sử dụng rộng rãi về lớp mà điều này sẽ quan trọng là OpenCV cv::mat. Hàm tạo mặc định cấp phát bộ nhớ mới, trong khi hàm tạo sao chép tạo một khung nhìn mới cho bộ nhớ hiện có.
jpa

Tôi sẽ không gọi đó là một ví dụ giả tạo, nó thể hiện rõ ràng sự khác biệt trong hành vi.
David Waterworth

51

Tiêu chuẩn có một danh sách các thay đổi đột phá trong Phụ lục C [khác] . Nhiều thay đổi này có thể dẫn đến thay đổi hành vi thầm lặng.

Một ví dụ:

int f(const char*); // #1
int f(bool);        // #2

int x = f(u8"foo"); // until C++20: calls #1; since C++20: calls #2

7
@einpoklum Chà, ít nhất một tá trong số chúng được cho là "thay đổi ý nghĩa" của mã hiện có hoặc khiến chúng "thực thi khác đi".
cpplearner

4
Làm thế nào bạn sẽ tóm tắt cơ sở lý do cho sự thay đổi cụ thể này?
Nayuki

4
@Nayuki khá chắc chắn rằng nó sử dụng boolphiên bản không phải là một thay đổi dự định, chỉ là một tác dụng phụ của các quy tắc chuyển đổi khác. Mục đích thực sự là để ngăn chặn một số nhầm lẫn giữa các mã hóa ký tự, sự thay đổi thực tế mà các u8ký tự được sử dụng để cung cấp const char*nhưng bây giờ cung cấp const char8_t*.
vòng trái vào

25

Mỗi khi họ thêm các phương thức mới (và thường là các hàm) vào thư viện chuẩn, điều này sẽ xảy ra.

Giả sử bạn có một loại thư viện chuẩn:

struct example {
  void do_stuff() const;
};

khá đơn giản. Trong một số bản sửa đổi tiêu chuẩn, một phương thức mới hoặc quá tải hoặc bên cạnh bất kỳ thứ gì được thêm vào:

struct example {
  void do_stuff() const;
  void method(); // a new method
};

điều này có thể âm thầm thay đổi hành vi của các chương trình C ++ hiện có.

Điều này là do khả năng phản chiếu hạn chế hiện tại của C ++ đủ để phát hiện ra phương thức như vậy có tồn tại hay không và chạy mã khác dựa trên nó.

template<class T, class=void>
struct detect_new_method : std::false_type {};

template<class T>
struct detect_new_method< T, std::void_t< decltype( &T::method ) > > : std::true_type {};

đây chỉ là một cách tương đối đơn giản để phát hiện cái mới method, có vô số cách.

void task( std::false_type ) {
  std::cout << "old code";
};
void task( std::true_type ) {
  std::cout << "new code";
};

int main() {
  task( detect_new_method<example>{} );
}

Điều tương tự cũng có thể xảy ra khi bạn xóa các phương thức khỏi các lớp.

Trong khi ví dụ này trực tiếp phát hiện ra sự tồn tại của một phương thức, thì kiểu điều này xảy ra gián tiếp có thể ít giả thiết hơn. Ví dụ cụ thể, bạn có thể có một công cụ tuần tự hóa quyết định xem thứ gì đó có thể được tuần tự hóa dưới dạng một vùng chứa hay không dựa trên việc liệu nó có thể lặp lại được hay không, hoặc nếu nó có một dữ liệu trỏ đến raw-byte và một thành viên kích thước, với một ưu tiên hơn cai khac.

Tiêu chuẩn đi và thêm một .data()phương thức vào một vùng chứa, và đột nhiên kiểu thay đổi đường dẫn mà nó sử dụng để tuần tự hóa.

Tất cả những gì tiêu chuẩn C ++ có thể làm, nếu nó không muốn bị đóng băng, là làm cho loại mã bị ngắt âm thầm trở nên hiếm hoặc bằng cách nào đó không hợp lý.


3
Tôi lẽ ra phải đủ điều kiện để câu hỏi loại trừ SFINAE vì đây không phải là ý của tôi ... nhưng vâng, đó là sự thật, vì vậy hãy +1.
einpoklum

"loại điều này xảy ra gián tiếp" dẫn đến một ủng hộ hơn là một phản đối vì nó là một cái bẫy thực sự.
Ian Ringrose

1
Đây là một ví dụ thực sự tốt. Mặc dù OP có ý định loại trừ nó, nhưng đây có lẽ là một trong những điều có khả năng gây ra các thay đổi hành vi im lặng nhất đối với mã hiện có. +1
cdhowie

1
@TedLyngmo Nếu bạn không thể sửa máy dò, hãy thay đổi thứ được phát hiện. Bắn súng cá mập Texas!
Yakk - Adam Nevraumont

15

Oh boy ... Các liên kết cpplearner cung cấpđáng sợ .

Trong số những người khác, C ++ 20 không cho phép khai báo cấu trúc kiểu C của cấu trúc C ++.

typedef struct
{
  void member_foo(); // Ill-formed since C++20
} m_struct;

Nếu bạn được dạy cách viết các cấu trúc như thế (và những người dạy "C với các lớp học" dạy chính xác điều đó) thì bạn đã bị hỏng .


19
Ai đã dạy điều đó nên viết 100 lần trên bảng đen "I would not typedef structs". Bạn thậm chí không nên làm điều đó trong C, imho. Dù sao đi nữa, sự thay đổi đó không phải là im lặng: Trong tiêu chuẩn mới, "Mã C ++ 2017 hợp lệ (sử dụng typedef trên các cấu trúc ẩn danh, không phải C) có thể bị lỗi""không đúng - chương trình có lỗi cú pháp hoặc lỗi ngữ nghĩa có thể chẩn đoán được . Cần phải có trình biên dịch C ++ phù hợp để đưa ra chẩn đoán " .
Peter - Phục hồi Monica

19
@ Peter-ReinstateMonica Chà, tôi luôn typedeflà cấu trúc của mình, và chắc chắn tôi sẽ không lãng phí phấn của mình cho nó. Đây chắc chắn là vấn đề sở thích, và trong khi có những người có ảnh hưởng lớn (Torvalds ...) chia sẻ quan điểm của bạn, những người khác như tôi sẽ chỉ ra rằng quy ước đặt tên cho các loại là tất cả những gì cần thiết. Việc lộn xộn mã với structcác từ khóa làm tăng thêm ít hiểu biết rằng một ký tự viết hoa ( MyClass* object = myClass_create();) sẽ không truyền đạt. Tôi tôn trọng nó nếu bạn muốn structmã của bạn. Nhưng tôi không muốn nó trong của tôi.
cmaster - phục hồi monica

5
Điều đó nói lên rằng, khi lập trình C ++, thực sự là một quy ước tốt là structchỉ sử dụng cho các kiểu dữ liệu thuần túy cũ và classbất kỳ thứ gì có các hàm thành viên. Nhưng bạn không thể sử dụng quy ước đó trong C vì không có classtrong C.
cmaster - phục hồi monica

1
@ Peter-ReinstateMonica Vâng, bạn không thể đính kèm cú pháp phương thức trong C, nhưng điều đó không có nghĩa là C structthực sự là POD. Theo cách tôi viết mã C, hầu hết các cấu trúc chỉ được chạm vào mã trong một tệp duy nhất và bởi các hàm mang tên lớp của chúng. Về cơ bản nó là OOP không có đường cú pháp. Điều này cho phép tôi thực sự kiểm soát những gì thay đổi bên trong a structvà những bất biến nào được đảm bảo giữa các thành viên của nó. Vì vậy, structsxu hướng của tôi có các hàm thành viên, triển khai riêng tư, bất biến và trừu tượng từ các thành viên dữ liệu của chúng. Nghe không giống POD, phải không?
cmaster - phục hồi monica

5
Miễn là chúng không bị cấm trong extern "C"các khối, tôi không thấy bất kỳ vấn đề nào với thay đổi này. Không ai nên gõ cấu trúc trong C ++. Đây không phải là trở ngại lớn hơn thực tế là C ++ có ngữ nghĩa khác với Java. Khi bạn học một ngôn ngữ lập trình mới, bạn có thể cần phải học một số thói quen mới.
Cody Grey

15

Đây là một ví dụ in 3 trong C ++ 03 nhưng 0 trong C ++ 11:

template<int I> struct X   { static int const c = 2; };
template<> struct X<0>     { typedef int c; };
template<class T> struct Y { static int const c = 3; };
static int const c = 4;
int main() { std::cout << (Y<X< 1>>::c >::c>::c) << '\n'; }

Sự thay đổi hành vi này là do xử lý đặc biệt cho >>. Trước C ++ 11, >>luôn là toán tử dịch đúng. Với C ++ 11, >>cũng có thể là một phần của khai báo mẫu.


Về mặt kỹ thuật thì điều này đúng, nhưng đoạn mã này bắt đầu "không chính thức" do việc sử dụng >>theo cách đó.
einpoklum

11

Các đoạn đã giảm

Tệp nguồn được mã hóa trong một tập ký tự vật lý được ánh xạ theo cách được triển khai xác định với tập ký tự nguồn , được định nghĩa trong tiêu chuẩn. Để phù hợp với các ánh xạ từ một số bộ ký tự vật lý vốn dĩ không có tất cả các dấu câu cần thiết cho bộ ký tự nguồn, các đoạn văn được xác định theo ngôn ngữ — chuỗi gồm ba ký tự phổ biến có thể được sử dụng thay cho một ký tự dấu câu ít phổ biến hơn. Bộ tiền xử lý và trình biên dịch được yêu cầu để xử lý những điều này.

Trong C ++ 17, các đồ thị đã bị loại bỏ. Vì vậy, một số tệp nguồn sẽ không được chấp nhận bởi các trình biên dịch mới hơn trừ khi chúng được dịch lần đầu tiên từ bộ ký tự vật lý sang một số bộ ký tự vật lý khác ánh xạ 1-1 sang bộ ký tự nguồn. (Trong thực tế, hầu hết các trình biên dịch chỉ thực hiện việc giải thích các đoạn văn là tùy chọn.) Đây không phải là một thay đổi hành vi tinh tế, mà là một thay đổi đột phá ngăn không cho các tệp nguồn được chấp nhận trước đó được biên dịch mà không cần quá trình dịch bên ngoài.

Nhiều ràng buộc hơn về char

Tiêu chuẩn cũng đề cập đến bộ ký tự thực thi , được xác định việc triển khai, nhưng phải chứa ít nhất toàn bộ bộ ký tự nguồn cộng với một số lượng nhỏ mã điều khiển.

Tiêu chuẩn C ++ được định nghĩa charlà một kiểu tích phân có thể không dấu có thể biểu diễn hiệu quả mọi giá trị trong bộ ký tự thực thi. Với sự trình bày từ một luật sư ngôn ngữ, bạn có thể lập luận rằng a charphải có ít nhất 8 bit.

Nếu việc triển khai của bạn sử dụng giá trị không được đánh dấu cho char, thì bạn biết nó có thể nằm trong khoảng từ 0 đến 255 và do đó phù hợp để lưu trữ mọi giá trị byte có thể có.

Nhưng nếu triển khai của bạn sử dụng giá trị đã ký, thì nó có các tùy chọn.

Hầu hết sẽ sử dụng phần bù của hai, cho charmột phạm vi tối thiểu từ -128 đến 127. Đó là 256 giá trị duy nhất.

Nhưng một tùy chọn khác là dấu + độ lớn, trong đó một bit được dành riêng để cho biết liệu số có âm hay không và bảy bit còn lại biểu thị độ lớn. Điều đó sẽ cung cấp charmột phạm vi từ -127 đến 127, chỉ là 255 giá trị duy nhất. (Bởi vì bạn mất một tổ hợp bit hữu ích để biểu diễn -0.)

Tôi không chắc ủy ban đã bao giờ chỉ định rõ ràng đây là một khiếm khuyết, nhưng đó là vì bạn không thể dựa vào tiêu chuẩn để đảm bảo một chuyến khứ hồi từ unsigned charđi charvà về sẽ giữ nguyên giá trị ban đầu. (Trong thực tế, tất cả các triển khai đều như vậy vì chúng đều sử dụng phần bù của hai cho các loại tích phân có dấu.)

Chỉ gần đây (C ++ 17?) Mới được sửa lại từ ngữ để đảm bảo sự thành thạo. Bản sửa lỗi đó, cùng với tất cả các yêu cầu khác trên char, bắt buộc một cách hiệu quả phần bù của hai cho charcó dấu mà không cần nói rõ ràng như vậy (ngay cả khi tiêu chuẩn tiếp tục cho phép biểu diễn dấu + độ lớn cho các loại tích phân có dấu khác). Có một đề xuất yêu cầu tất cả các kiểu tích phân có dấu sử dụng phần bù của hai, nhưng tôi không nhớ liệu nó có được đưa vào C ++ 20 hay không.

Vì vậy, điều này tương tự như những gì bạn đang tìm kiếm bởi vì nó cung cấp cho mã quá tự tin không chính xác trước đó một bản sửa lỗi có hiệu lực trở lại.


Phần tam đoạn luận không phải là câu trả lời cho câu hỏi này - đó không phải là một sự thay đổi thầm lặng. Và, IIANM, phần thứ hai là một sự thay đổi của hành vi được xác định thực hiện thành hành vi được ủy quyền nghiêm ngặt, đây cũng không phải là điều tôi đã hỏi.
einpoklum

10

Tôi không chắc liệu bạn có coi đây là một thay đổi vi phạm để sửa mã hay không, nhưng ...

Trước C ++ 11, các trình biên dịch được phép, nhưng không bắt buộc, để xử lý các bản sao trong một số trường hợp nhất định, ngay cả khi trình tạo bản sao có các tác dụng phụ có thể quan sát được. Bây giờ chúng tôi đã đảm bảo tách bản sao. Hành vi về cơ bản đi từ việc triển khai được xác định đến bắt buộc.

Điều này có nghĩa là các tác dụng phụ của hàm tạo bản sao của bạn có thể đã xảy ra với các phiên bản cũ hơn, nhưng sẽ không bao giờ xảy ra với các phiên bản mới hơn. Bạn có thể tranh luận rằng mã đúng không nên dựa vào kết quả do triển khai xác định, nhưng tôi không nghĩ rằng điều đó hoàn toàn giống với việc nói mã như vậy là không chính xác.


1
Tôi nghĩ rằng "yêu cầu" này đã được thêm vào C ++ 17, không phải C ++ 11? (Xem tài liệu hóa tạm thời .)
cdhowie

@cdhowie: Tôi nghĩ bạn đúng. Tôi đã không có sẵn các tiêu chuẩn khi viết bài này và có lẽ tôi đã quá tin tưởng vào một số kết quả tìm kiếm của mình.
Adrian McCarthy

Thay đổi đối với hành vi do triển khai xác định không được coi là câu trả lời cho câu hỏi này.
einpoklum

7

Hành vi khi đọc dữ liệu (số) từ một luồng và đọc không thành công, đã được thay đổi kể từ c ++ 11.

Ví dụ: đọc một số nguyên từ một luồng, trong khi nó không chứa một số nguyên:

#include <iostream>
#include <sstream>

int main(int, char **) 
{
    int a = 12345;
    std::string s = "abcd";         // not an integer, so will fail
    std::stringstream ss(s);
    ss >> a;
    std::cout << "fail = " << ss.fail() << " a = " << a << std::endl;        // since c++11: a == 0, before a still 12345 
}

Vì c ++ 11 sẽ đặt số nguyên đọc thành 0 khi nó bị lỗi; tại c ++ <11 số nguyên không bị thay đổi. Điều đó nói rằng, gcc, ngay cả khi buộc tiêu chuẩn trở lại c ++ 98 (với -std = c ++ 98) luôn hiển thị hành vi mới ít nhất kể từ phiên bản 4.4.7.

(Imho hành vi cũ thực sự tốt hơn: tại sao lại thay đổi giá trị thành 0, tự nó là hợp lệ, khi không thể đọc được gì?)

Tham khảo: xem https://en.cppreference.com/w/cpp/locale/num_get/get


Nhưng không có thay đổi nào được đề cập về returnType. Chỉ có 2 tin tức quá tải kể từ C ++ 11
Xây dựng thành công

Hành vi này có được xác định cả trong C ++ 98 và C ++ 11 không? Hay hành vi đã được xác định?
einpoklum

Khi cppreference.com đúng: "nếu xảy ra lỗi, v được giữ nguyên. (Cho đến khi C ++ 11)" Vì vậy, hành vi đã được định nghĩa trước C ++ 11 và đã thay đổi.
DanRechtsaf

Theo sự hiểu biết của tôi, hành vi cho ss> a thực sự đã được xác định, nhưng đối với trường hợp rất phổ biến khi bạn đang đọc một biến chưa được khởi tạo, hành vi c ++ 11 sẽ sử dụng một biến chưa được khởi tạo, đó là hành vi không được xác định. Do đó, cấu trúc mặc định trên failiure bảo vệ chống lại một hành vi không xác định rất phổ biến.
Rasmus Damgaard Nielsen
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.