Hành vi không xác định và các điểm trình tự được tải lại


84

Hãy coi chủ đề này là phần tiếp theo của chủ đề sau:

Phần trước
Hành vi không xác định và các điểm trình tự

Hãy xem lại biểu cảm hài hướcphức tạp này (các cụm từ in nghiêng được lấy từ chủ đề trên * smile *):

i += ++i;

Chúng tôi nói rằng điều này gọi hành vi không xác định. Tôi cho rằng khi nói điều này, chúng ta mặc nhiên cho rằng kiểu của ilà một trong những kiểu cài sẵn.

Điều gì xảy ra nếu kiểu của ilà kiểu do người dùng xác định? Giả sử loại của nó Indexđược xác định sau trong bài đăng này (xem bên dưới). Nó sẽ vẫn gọi hành vi không xác định?

Nếu đúng thì tại sao? Nó không tương đương với viết i.operator+=(i.operator++());hoặc thậm chí đơn giản hơn về mặt cú pháp i.add(i.inc());? Hoặc, họ cũng gọi hành vi không xác định?

Nếu không, tại sao không? Rốt cuộc, đối tượng iđược sửa đổi hai lần giữa các điểm trình tự liên tiếp. Vui lòng nhớ lại quy tắc ngón tay cái: một biểu thức chỉ có thể sửa đổi giá trị của đối tượng một lần giữa các điểm trình tự "liên tiếp . Và nếu i += ++ilà một biểu thức, thì nó phải gọi hành vi không xác định. Nếu vậy, thì tương đương của nó i.operator+=(i.operator++());i.add(i.inc());cũng phải gọi hành vi không xác định mà dường như là không đúng sự thật! (theo như tôi hiểu)

Hoặc, i += ++ikhông phải là một biểu thức để bắt đầu? Nếu vậy, thì nó là gì và định nghĩa của biểu thức là gì?

Nếu đó là một biểu thức và đồng thời, hành vi của nó cũng được xác định rõ ràng, thì điều đó ngụ ý rằng số lượng điểm trình tự được liên kết với một biểu thức bằng cách nào đó phụ thuộc vào loại toán hạng liên quan đến biểu thức. Tôi có đúng không (thậm chí một phần)?


Nhân tiện, làm thế nào về biểu hiện này?

//Consider two cases:
//1. If a is an array of a built-in type
//2. If a is user-defined type which overloads the subscript operator!

a[++i] = i; //Taken from the previous topic. But here type of `i` is Index.

Bạn cũng phải cân nhắc điều này trong phản hồi của mình (nếu bạn biết chắc chắn hành vi của nó). :-)


++++++i;

được xác định rõ trong C ++ 03? Rốt cuộc, đây là cái này,

((i.operator++()).operator++()).operator++();

class Index
{
    int state;

    public:
        Index(int s) : state(s) {}
        Index& operator++()
        {
            state++;
            return *this;
        }
        Index& operator+=(const Index & index)
        {
            state+= index.state;
            return *this;
        }
        operator int()
        {
            return state;
        }
        Index & add(const Index & index)
        {
            state += index.state;
            return *this;
        }
        Index & inc()
        {
            state++;
            return *this;
        }
};

13
+1 câu hỏi tuyệt vời, tạo cảm hứng cho những câu trả lời tuyệt vời. Tôi cảm thấy tôi phải nói rằng nó vẫn còn mã khủng khiếp mà nên được refactored để có thể đọc nhiều hơn, nhưng bạn có thể biết rằng anyway :)
Philip Potter

4
@ Câu hỏi là gì: ai nói nó giống nhau? hoặc ai nói nó không giống nhau? Nó không phụ thuộc vào cách bạn thực hiện chúng như thế nào? (Lưu ý: Tôi giả sử loại là loại do sngười dùng xác định!)
Nawaz

5
Tôi không thấy bất kỳ vô hướng đối tượng được điều chỉnh hai lần giữa hai điểm chuỗi ...
Johannes Schaub - litb

3
@Johannes: sau đó là về đối tượng vô hướng . Nó là gì? Tôi tự hỏi tại sao tôi chưa bao giờ nghe nói về nó trước đây. Có thể, vì các hướng dẫn / C ++ - faq không đề cập đến nó, hoặc không nhấn mạnh nó? Nó có gì khác với các đối tượng của kiểu dựng sẵn ?
Nawaz

3
@Phillip: Rõ ràng là tôi sẽ không viết mã như vậy trong đời thực; trong thực tế, không có lập trình viên lành mạnh nào sẽ viết nó. Những câu hỏi này thường được đặt ra để chúng ta có thể hiểu toàn bộ hoạt động kinh doanh của hành vi không xác định và các điểm trình tự tốt hơn! :-)
Nawaz

Câu trả lời:


48

Nó trông giống như mã

i.operator+=(i.operator ++());

Hoạt động hoàn toàn tốt liên quan đến các điểm trình tự. Mục 1.9.17 của tiêu chuẩn ISO C ++ nói điều này về các điểm trình tự và đánh giá chức năng:

Khi gọi một hàm (cho dù hàm có nội tuyến hay không), có một điểm trình tự sau khi đánh giá tất cả các đối số của hàm (nếu có) diễn ra trước khi thực thi bất kỳ biểu thức hoặc câu lệnh nào trong thân hàm. Cũng có một điểm trình tự sau khi sao chép một giá trị trả về và trước khi thực hiện bất kỳ biểu thức nào bên ngoài hàm.

Ví dụ, điều này chỉ ra rằng i.operator ++()tham số operator +=có một điểm trình tự sau khi đánh giá. Tóm lại, bởi vì các toán tử được nạp chồng là các hàm, các quy tắc sắp xếp trình tự thông thường được áp dụng.

Nhân tiện, câu hỏi tuyệt vời! Tôi thực sự thích cách bạn buộc tôi phải hiểu tất cả các sắc thái của một ngôn ngữ mà tôi đã nghĩ rằng tôi đã biết (và nghĩ rằng tôi nghĩ rằng tôi biết). :-)



11

Như những người khác đã nói, i += ++iví dụ của bạn hoạt động với kiểu do người dùng xác định vì bạn đang gọi các hàm và các hàm bao gồm các điểm trình tự.

Mặt khác, a[++i] = ikhông may mắn như vậy nếu giả sử đó alà kiểu mảng cơ bản của bạn hoặc thậm chí là do người dùng xác định. Vấn đề bạn gặp phải ở đây là chúng tôi không biết phần nào của biểu thức chứa iđược đánh giá trước. Nó có thể được ++iđánh giá, chuyển cho operator[](hoặc phiên bản thô) để truy xuất đối tượng ở đó, và sau đó giá trị của iđược chuyển cho đối tượng đó (sau khi tôi được tăng lên). Mặt khác, có lẽ phần sau được đánh giá trước, được lưu trữ để gán sau, và sau đó ++iphần được đánh giá.


do đó ... do đó kết quả là không xác định chứ không phải UB, vì thứ tự đánh giá các biểu thức là không xác định?
Philip Potter

@Philip: không xác định có nghĩa là chúng tôi mong đợi trình biên dịch chỉ định hành vi, trong khi những nơi không xác định không có nghĩa vụ như vậy. Tôi nghĩ rằng nó không được xác định ở đây, để cho phép các trình biên dịch có thêm không gian để tối ưu hóa.
Matthieu M.

@Noah: Tôi cũng đã đăng một phản hồi. Hãy kiểm tra nó ra và cho tôi biết suy nghĩ của bạn. :-)
Nawaz

1
@Philip: kết quả là UB, vì quy tắc ở 5/4: "Các yêu cầu của đoạn này phải được đáp ứng cho từng thứ tự cho phép của các biểu thức phụ của một biểu thức đầy đủ; nếu không thì hành vi là không xác định.". Nếu tất cả các câu lệnh cho phép có các điểm trình tự giữa lần sửa đổi ++ivà việc đọc iRHS của bài tập, thì thứ tự sẽ không được xác định. Bởi vì một trong những thử thách được phép thực hiện hai điều đó mà không có điểm trình tự can thiệp, hành vi là không xác định.
Steve Jessop,

1
@Philip: Nó không chỉ định nghĩa hành vi không xác định là hành vi không xác định. Một lần nữa, nếu phạm vi hành vi không xác định bao gồm một số hành vi không xác định, thì hành vi tổng thể là không xác định. Nếu phạm vi của hành vi không xác định được xác định trong tất cả các khả năng, thì hành vi tổng thể là không xác định. Nhưng bạn nói đúng ở điểm thứ hai, tôi đã nghĩ đến một anội dung và do người dùng xác định i.
Steve Jessop,

8

Tôi nghĩ nó được xác định rõ ràng:

Từ tiêu chuẩn nháp C ++ (n1905) §1.9 / 16:

"Cũng có một điểm trình tự sau khi sao chép một giá trị trả về và trước khi thực thi bất kỳ biểu thức nào bên ngoài hàm13). Một số ngữ cảnh trong C ++ gây ra đánh giá về một lệnh gọi hàm, mặc dù không có cú pháp gọi hàm tương ứng nào xuất hiện trong đơn vị dịch. [ Ví dụ : đánh giá một biểu thức mới gọi một hoặc nhiều hàm cấp phát và hàm tạo; xem 5.3.4. Ví dụ khác, việc gọi hàm chuyển đổi (12.3.2) có thể phát sinh trong các ngữ cảnh mà không có cú pháp gọi hàm nào xuất hiện. - end ví dụ ] Các điểm trình tự tại mục nhập hàm và thoát hàm (như mô tả ở trên) là các đặc điểm của các lệnh gọi hàm như được đánh giá, bất kể cú pháp của biểu thứcmà có thể gọi hàm. "

Lưu ý phần tôi đã in đậm. Điều này có nghĩa là thực sự có một điểm trình tự sau lời gọi hàm tăng ( i.operator ++()) nhưng trước lời gọi gán ghép ( i.operator+=).


6

Ổn thỏa. Sau khi xem qua các câu trả lời trước đó, tôi nghĩ lại câu hỏi của mình, đặc biệt là phần này mà chỉ có Noah cố gắng trả lời nhưng tôi không bị thuyết phục hoàn toàn với anh ấy.

a[++i] = i;

Trường hợp 1:

If alà một mảng kiểu dựng sẵn. Vậy thì những gì Nô-ê nói là đúng. Đó là,

a [++ i] = tôi không may mắn lắm khi cho rằng a là kiểu mảng cơ bản của bạn, hoặc thậm chí một người dùng đã xác định một . Vấn đề bạn gặp phải ở đây là chúng tôi không biết phần nào của biểu thức chứa i được đánh giá đầu tiên.

Vì vậy, a[++i]=igọi hành vi không xác định, hoặc kết quả là không xác định. Dù nó là gì, nó không được xác định rõ ràng!

PS: Trong báo giá trên, đình công tất nhiên là của tôi.

Trường hợp 2:

Nếu alà một đối tượng của kiểu do người dùng xác định sẽ làm quá tải operator[], thì lại có hai trường hợp.

  1. Nếu kiểu trả về của operator[]hàm quá tải là kiểu tích hợp, thì một lần nữa a[++i]=isẽ gọi hành vi không xác định hoặc kết quả là không xác định.
  2. Nhưng nếu kiểu trả về của operator[]hàm được nạp chồng là kiểu do người dùng định nghĩa, thì hành vi của a[++i] = iđược xác định rõ ràng (theo như tôi hiểu), vì trong trường hợp a[++i]=inày tương đương với cách viết a.operator[](++i).operator=(i);giống như a[++i].operator=(i);,. Nghĩa là, phép gán operator=được gọi trên đối tượng trả về, đối tượng a[++i]này dường như được xác định rất rõ ràng, vì tại thời điểm a[++i]trả về, ++iđã được đánh giá, và sau đó đối tượng trả về gọi operator=hàm truyền giá trị cập nhật của inó làm đối số. Lưu ý rằng có một điểm trình tự giữa hai cuộc gọi này . Và cú pháp đảm bảo rằng không có sự cạnh tranh giữa hai cuộc gọi này vàoperator[]sẽ được gọi đầu tiên, và liên tiếp, đối số ++iđược chuyển vào đó, cũng sẽ được đánh giá đầu tiên.

Hãy nghĩ về điều này như someInstance.Fun(++k).Gun(10).Sun(k).Tun();trong đó mỗi lệnh gọi hàm liên tiếp trả về một đối tượng của một số kiểu do người dùng xác định. Đối với tôi, tình huống này có vẻ giống như thế này eat(++k);drink(10);sleep(k):, bởi vì trong cả hai tình huống, tồn tại điểm trình tự sau mỗi lần gọi hàm.

Hãy sửa lại cho tôi nếu tôi sai. :-)


1
@Nawaz k++kđang không tách ra bởi các điểm theo thứ tự. Cả hai đều có thể được đánh giá trước khi Sunhoặc Funđược đánh giá. Ngôn ngữ chỉ yêu cầu đối số Funđược đánh giá trước Sun, không phải Funđối số của đó được đánh giá trước Sunđối số. Tôi đang giải thích điều tương tự một lần nữa mà không thể cung cấp tài liệu tham khảo, vì vậy chúng tôi sẽ không tiến bộ từ đây.
Philip Potter

1
@Nawaz: vì không có gì xác định điểm trình tự phân tách chúng. Có các điểm trình tự trước và sau khi Sunthực thi, nhưng Funđối số của ++kcó thể được đánh giá trước hoặc sau đó. Có các điểm trình tự trước và sau khi Funthực thi, nhưng Sunđối số của kcó thể được đánh giá trước hoặc sau đó. Do đó, một trường hợp có thể xảy ra là cả hai k++kđều được đánh giá trước một trong hai Sunhoặc Funđược đánh giá, và vì vậy cả hai đều nằm trước các điểm trình tự lệnh gọi hàm, và do đó không có điểm trình tự nào tách biệt k++k.
Philip Potter

1
@Philip: Tôi nhắc lại: tình huống này có gì khác với eat(i++);drink(10);sleep(i);? ... ngay cả bây giờ, bạn có thể nói i++có thể được đánh giá trước hoặc sau đó?
Nawaz

1
@Nawaz: làm cách nào để tôi có thể nói rõ hơn? Trong ví dụ Fun / Sun, không có điểm trình tự nào giữa k++k. Trong ví dụ ăn / uống, có như điểm chuỗi giữa ii++.
Philip Potter

3
@Philip: điều đó không có ý nghĩa gì cả. Giữa Fun () và Sun () tồn tại một điểm trình tự, nhưng giữa đối số của chúng không tồn tại điểm trình tự. Nó giống như nói, giữa eat()sleep()tồn tại (các) điểm trình tự, nhưng giữa các đối số đó thậm chí không phải là một. Làm thế nào để các đối số của hai lệnh gọi hàm được phân tách bằng các điểm trình tự, thuộc cùng các điểm trình tự?
Nawaz
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.