簡體   English   中英

在 C++17 中的對象生命周期之外調用非靜態成員 function

[英]Calling non-static member function outside of object's lifetime in C++17

以下程序在 C++17 及更高版本中是否具有未定義的行為?

struct A {
    void f(int) { /* Assume there is no access to *this here */ }
};

int main() {
    auto a = new A;
    a->f((a->~A(), 0));
}

C++17 保證a->f在調用的參數被評估之前被評估為A object 的成員 function 。 因此,來自->的間接定義是明確的。 但在輸入 function 調用之前,會評估參數並結束A object 的生命周期(但請參見下面的編輯)。 調用是否仍有未定義的行為? 是否可以通過這種方式在其生命周期之外調用 object 的成員 function ?

a->f的值類別是[expr.ref]/6.3.2的 prvalue,並且[basic.life]/7僅不允許非靜態成員function調用引用生命周期后 object 的 glvalues。 這是否意味着呼叫有效? (編輯:正如評論中所討論的,我可能誤解了 [basic.life]/7,它可能確實適用於此。)

如果我將析構函數調用a->~A()替換為delete anew(a) A (使用#include<new> ),答案是否會改變?


對我的問題進行了一些詳細的編輯和澄清:


如果我將成員 function 調用和 destructor/delete/placement-new 分成兩個語句,我認為答案很明確:

  1. a->A(); a->f(0) a->A(); a->f(0) :UB,因為在其生命周期之外a非靜態成員調用。 (但請參閱下面的編輯)
  2. delete a; a->f(0) delete a; a->f(0) : 同上
  3. new(a) A; a->f(0) new(a) A; a->f(0) : 定義明確,調用新的 object

然而,在所有這些情況下, a->f都在第一個相應的語句之后進行排序,而在我最初的示例中這個順序是相反的。 我的問題是這種逆轉是否允許改變答案?


對於 C++17 之前的標准,我最初認為所有三種情況都會導致未定義的行為,因為a->f的評估取決於a的值,但相對於對 a 造成副作用的參數的評估是無序a . 但是,僅當標量值存在實際副作用時,這才是未定義的行為,例如寫入標量 object。 但是,沒有寫入標量 object 因為A是微不足道的,因此我也會對在 C++17 之前的標准的情況下究竟違反了什么約束感興趣。 特別是,placement-new 的情況現在對我來說似乎還不清楚。


我剛剛意識到關於對象生命周期的措辭在 C++17 和當前草案之間發生了變化。 在 n4659(C++17 草案)[basic.life]/1 中說:

T 型 object o 的生命周期在以下情況下結束:

  • 如果 T 是具有非平凡析構函數 (15.4) 的 class 類型,則析構函數調用開始

[...]

目前的草案說:

T 型 object o 的生命周期在以下情況下結束:

[...]

  • 如果 T 是 class 類型,則析構函數調用開始,或者

[...]

因此,我想我的示例在 C++17 中確實具有明確定義的行為,但在當前的 (C++20) 草案中沒有,因為析構函數調用是微不足道的,並且A object 的生命周期沒有結束。 我也希望對此作出澄清。 我最初的問題仍然適用於 C++17 對於用 delete 或 placement-new 表達式替換析構函數調用的情況。


如果f在其主體中訪問*this ,則對於析構函數調用和刪除表達式的情況可能存在未定義的行為,但是在這個問題中,我想關注調用本身是否有效。 但是請注意,我的問題與 placement-new 的變化可能不會對f中的成員訪問有問題,這取決於調用本身是否是未定義的行為。 但在那種情況下,可能會有一個后續問題,特別是對於放置新的情況,因為我不清楚 function 中的this是否會始終自動引用新的 object 或者它是否可能需要潛在地是std::launder ed(取決於A有什么成員)。


雖然A確實有一個微不足道的析構函數,但更有趣的情況可能是它有一些副作用,編譯器可能希望為優化目的做出假設。 (我不知道是否有任何編譯器使用這樣的東西。)因此,我歡迎A也具有非平凡析構函數的情況的答案,特別是如果兩種情況的答案不同。

此外,從實際的角度來看,一個微不足道的析構函數調用可能不會影響生成的代碼和(不太可能?)基於未定義行為假設的優化,所有代碼示例很可能生成在大多數編譯器上按預期運行的代碼。 我對理論更感興趣,而不是這種實踐視角。


這個問題旨在更好地理解語言的細節。 我不鼓勵任何人編寫這樣的代碼。

后綴表達式a->f在任何 arguments 的評估之前進行排序(相對於彼此不確定地排序)。 (見[expr.call])

arguments 的評估在 function 的主體之前排序(甚至內聯函數,請參閱 [intro.execution])

這意味着調用 function 本身並不是未定義的行為。 但是,訪問任何成員變量或調用其中的其他成員函數將是每個 [basic.life] 的 UB。

所以結論是,這個特定的實例按照措辭是安全的,但總的來說是一種危險的技術。

確實,在 C++20(計划)之前,瑣碎的析構函數根本什么都不做,甚至沒有結束 object 的生命周期。 所以問題是,呃,微不足道的,除非我們假設一個非平凡的析構函數或像delete這樣更強大的東西。

在這種情況下,C++17 的排序沒有幫助:調用(不是 class 成員訪問)使用指向 object 的指針(來初始化this ),違反了過期指針的規則

旁注:如果只有一個訂單未定義,那么 C++17 之前的“未指定訂單”也是如此:如果未指定行為的任何可能性是未定義行為,則該行為未定義。 (你怎么知道選擇了明確定義的選項?未定義的可以模仿它,然后釋放鼻惡魔。)

您似乎假設a->f(0)具有以下步驟(按照最新的 C++ 標准的順序,對於以前的版本按邏輯順序排列):

  • 評估*a
  • 評估a->f (所謂的綁定成員函數)
  • 評估0
  • 在參數列表(0)上調用綁定成員 function a->f

但是a->f既沒有值也沒有類型。 它本質上是一個無意義的語法元素,因為語法分解了成員訪問和 function 調用,即使在成員 function 調用上,它通過定義組合成員訪問和 ZC1C425268E68385D1CAB50 調用

所以問什么時候a->f被“評估”是一個沒有意義的問題:對於a->f value-less, type-less expression 沒有一個獨特的評估步驟

因此,任何基於非實體評估順序討論的推理也是無效的,null。

編輯:

實際上這比我寫的更糟糕,表達式a->f有一個虛假的“類型”:

E1.E2 是“參數類型列表 cv 返回 T 的函數”。

“參數類型列表 cv 的函數”甚至不是 class 之外的有效聲明符:不能像在全局聲明中那樣將f() const作為聲明符:

int ::f() const; // meaningless

並且在 class f() const內部並不意味着“參數類型列表 = () 且 cv=const 的函數”,它表示成員函數(參數類型列表 = () 且 cv=const 的函數)。沒有正確的“參數類型列表 cv 函數”的正確聲明符。它只能存在於 class 中;沒有可以聲明或真正可計算的類型“參數類型列表 cv 返回 T 的函數”表達式可以有。

除了別人說的:

a->~A(); 刪除一個;

這個程序有一個 memory 泄漏,它本身在技術上不是未定義的行為。 但是,如果您調用delete a; 為了防止它 - 這應該是未定義的行為,因為delete會第二次調用a->~A() [第 12.4/14 節]。

a->~A()

否則實際上這就像其他人建議的那樣 - 編譯器生成機器代碼A* a = malloc(sizeof(A)); a->A(); a->~A(); a->f(0); A* a = malloc(sizeof(A)); a->A(); a->~A(); a->f(0); . 由於沒有成員變量或虛函數,所有三個成員函數都是空的( {return;} )並且什么都不做。 指針a仍然指向有效的 memory。 它會運行,但調試器可能會抱怨 memory 泄漏。

但是,在f()中使用任何非靜態成員變量可能是未定義的行為,因為您是在編譯器生成的~A() (隱式)銷毀它們之后訪問它們 如果它類似於std::stringstd::vector ,這可能會導致運行時錯誤。

刪除一個

如果您將a->~A()替換為調用delete a; 相反,我相信這將是未定義的行為,因為此時指針a不再有效。

盡管如此,代碼應該仍然可以正常運行,因為 function f()是空的。 如果它訪問任何成員變量,它可能已經崩潰或導致隨機結果,因為a被釋放。

新的(a) A

auto a = new A; new(a) A; 本身就是未定義的行為,因為您為同一個 memory 第二次調用A()

在這種情況下,單獨調用 f() 將是有效的,因為a存在但構造a兩次是 UB。

如果A不包含任何具有分配 memory 等的構造函數的對象,它將運行良好。 否則可能會導致 memory 泄漏等,但 f() 可以訪問它們的“第二個”副本就好了。

我不是語言律師,但我拿走了你的代碼片段並稍微修改了一下。 我不會在生產代碼中使用它,但這似乎會產生有效的定義結果......

#include <iostream>
#include <exception>

struct A {
    int x{5};
    void f(int){}
    int g() { std::cout << x << '\n'; return x; }
};

int main() {
    try {
        auto a = new A;
        a->f((a->~A(), a->g()));
    catch(const std::exception& e) {
        std::cerr << e.what();
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

我正在運行 Visual Studio 2017 CE,編譯器語言標志設置為/std:c++latest ,我的 IDE 版本是15.9.16 ,我得到以下控制台 output 並退出程序狀態:

控制台 output

5

IDE 退出狀態 output

The program '[4128] Test.exe' has exited with code 0 (0x0).

所以這似乎是在 Visual Studio 的情況下定義的,我不確定其他編譯器將如何處理它。 正在調用析構函數,但變量a仍在動態堆 memory 中。


讓我們嘗試另一個輕微的修改:

#include <iostream>
#include <exception>

struct A {
    int x{5};
    void f(int){}
    int g(int y) { x+=y; std::cout << x << '\n'; return x; }
};

int main() {
    try {
        auto a = new A;
        a->f((a->~A(), a->g(3)));
    catch(const std::exception& e) {
        std::cerr << e.what();
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

控制台 output

8

IDE 退出狀態 output

The program '[4128] Test.exe' has exited with code 0 (0x0).

這次我們不要再更改class,而是讓我們之后打電話給a的成員......

int main() {
    try {
        auto a = new A;
        a->f((a->~A(), a->g(3)));
        a->g(2);
    } catch( const std::exception& e ) {
        std::cerr << e.what();
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

控制台 output

8
10

IDE 退出狀態 output

The program '[4128] Test.exe' has exited with code 0 (0x0).

在這里,似乎ax在調用a->~A()后保持其值,因為在A上調用了new並且尚未調用delete


如果我刪除new的並使用堆棧指針而不是分配的動態堆 memory,則更甚:

int main() {
    try {
        A b;
        A* a = &b;    
        a->f((a->~A(), a->g(3)));
        a->g(2);
    } catch( const std::exception& e ) {
        std::cerr << e.what();
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

我仍然得到:

控制台 output

8
10

IDE 退出狀態 output


當我將編譯器的語言標志設置從/c:std:c++latest更改為/std:c++17 ,我得到了相同的確切結果。

我從 Visual Studio 中看到的內容似乎定義明確,而沒有在我所展示的上下文中產生任何 UB。 但是,從語言的角度來看,當它涉及標准時,我也不會依賴這種類型的代碼。 上述內容也沒有考慮 class 具有內部指針時,堆棧自動存儲以及動態堆分配以及構造函數是否對這些內部對象調用 new 而析構函數對它們調用 delete。

除了編譯器的語言設置之外,還有許多其他因素,例如優化、約定調用和其他各種編譯器標志。 很難說,我沒有完整的最新起草標准的可用副本來更深入地調查這個問題。 也許這可以幫助您、其他能夠更徹底地回答您的問題的人以及其他讀者形象化這種行為。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM