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

前端 未结 5 1592
无人及你
无人及你 2021-02-03 22:06

Does the following program have undefined behavior in C++17 and later?

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

int main(         


        
相关标签:
5条回答
  • 2021-02-03 22:38

    In addition to what others said:

    a->~A(); delete a;

    This program has a memory leak which itself is technically not undefined behavior. However, if you called delete a; to prevent it - that should have been undefined behavior because delete would call a->~A() second time [Section 12.4/14].

    a->~A()

    Otherwise in reality this is as others suggested - compiler generates machine code along the lines of A* a = malloc(sizeof(A)); a->A(); a->~A(); a->f(0);. Since no member variables or virtuals all three member functions are empty ({return;}) and do nothing. Pointer a even still points to valid memory. It will run but debugger may complain of memory leak.

    However, using any nonstatic member variables inside f() could have been undefined behavior because you are accessing them after they are (implicitly) destroyed by compiler-generated ~A(). That would likely result in a runtime error if it was something like std::string or std::vector.

    delete a

    If you replaced a->~A() with expression that invoked delete a; instead then I believe this would have been undefined behavior because pointer a is no longer valid at that point.

    Despite that, the code should still run without errors because function f() is empty. If it accessed any member variables it may have crashed or led to random results because the memory for a is deallocated.

    new(a) A

    auto a = new A; new(a) A; is itself undefined behavior because you are calling A() a second time for the same memory.

    In that case calling f() by itself would be valid because a exists but constructing a twice is UB.

    It will run fine if A does not contain any objects with constructors allocating memory and such. Otherwise it could lead to memory leaks, etc, but f() would access the "second" copy of them just fine.

    0 讨论(0)
  • 2021-02-03 22:44

    The postfix expression a->f is sequenced before the evaluation of any arguments (which are indeterminately sequenced relative to one another). (See [expr.call])

    The evaluation of the arguments is sequenced before the body of the function (even inline functions, see [intro.execution])

    The implication, then is that calling the function itself is not undefined behavior. However, accessing any member variables or calling other member functions within would be UB per [basic.life].

    So the conclusion is that this specific instance is safe per the wording, but a dangerous technique in general.

    0 讨论(0)
  • 2021-02-03 22:46

    You seem to assume that a->f(0) has these steps (in that order for most recent C++ standard, in some logical order for previous versions):

    • evaluating *a
    • evaluating a->f (a so called bound member function)
    • evaluating 0
    • calling the bound member function a->f on the argument list (0)

    But a->f doesn't have either a value or type. It's essentially a non-thing, a meaningless syntax element needed only because the grammar decomposes member access and function call, even on a member function call which by define combines member access and function call.

    So asking when a->f is "evaluated" is a meaningless question: there is no such thing as a distinct evaluation step for the a->f value-less, type-less expression.

    So any reasoning based on such discussions of order of evaluation of non entity is also void and null.

    EDIT:

    Actually this is worse than what I wrote, the expression a->f has a phony "type":

    E1.E2 is “function of parameter-type-list cv returning T”.

    "function of parameter-type-list cv" isn't even something that would be a valid declarator outside a class: one cannot have f() const as a declarator as in a global declaration:

    int ::f() const; // meaningless
    

    And inside a class f() const doesn't mean "function of parameter-type-list=() with cv=const”, it means member-function (of parameter-type-list=() with cv=const). There is no proper declarator for proper "function of parameter-type-list cv". It can only exist inside a class; there is no type "function of parameter-type-list cv returning T" that can be declared or that real computable expressions can have.

    0 讨论(0)
  • 2021-02-03 22:46

    I'm not a language lawyer but I took your code snippet and modified it slightly. I wouldn't use this in production code but this seems to produce valid defined results...

    #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;
    }
    

    I'm running Visual Studio 2017 CE with compiler language flag set to /std:c++latest and my IDE's version is 15.9.16 and I get the follow console output and exit program status:

    console output

    5
    

    IDE exit status output

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

    So this does seem to be defined in the case of Visual Studio, I'm not sure how other compilers will treat this. The destructor is being invoked, however the variable a is still in dynamic heap memory.


    Let's try another slight modification:

    #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;
    }
    

    console output

    8
    

    IDE exit status output

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

    This time let's not change the class anymore, but let's make call on a's member afterwards...

    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;
    }
    

    console output

    8
    10
    

    IDE exit status output

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

    Here it appears that a.x is maintaining its value after a->~A() is called since new was called on A and delete has not yet been called.


    Even more if I remove the new and use a stack pointer instead of allocated dynamic heap 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;
    }
    

    I'm still getting:

    console output

    8
    10
    

    IDE exit status output


    When I change my compiler's language flag setting from /c:std:c++latest to /std:c++17 I'm getting the same exact results.

    What I'm seeing from Visual Studio it appears to be well defined without producing any UB within the contexts of what I've shown. However as from a language perspective when it concerns the standard I wouldn't rely on this type of code either. The above also doesn't consider when the class has internal pointers both stack-automatic storage as well as dynamic-heap allocation and if the constructor calls new on those internal objects and the destructor calls delete on them.

    There are also a bunch of other factors than just the language setting for the compiler such as optimizations, convention calling, and other various compiler flags. It is hard to say and I don't have an available copy of the full latest drafted standard to investigate this any deeper. Maybe this can help you, others who are able to answer your question more thoroughly, and other readers to visualize this type of behavior in action.

    0 讨论(0)
  • 2021-02-03 23:01

    It’s true that trivial destructors do nothing at all, not even end the lifetime of the object, prior to (the plans for) C++20. So the question is, er, trivial unless we suppose a non-trivial destructor or something stronger like delete.

    In that case, C++17’s ordering doesn’t help: the call (not the class member access) uses a pointer to the object (to initialize this), in violation of the rules for out-of-lifetime pointers.

    Side note: if just one order were undefined, so would be the “unspecified order” prior to C++17: if any of the possibilities for unspecified behavior are undefined behavior, the behavior is undefined. (How would you tell the well-defined option was chosen? The undefined one could emulate it and then release the nasal demons.)

    0 讨论(0)
提交回复
热议问题