Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update the example of temporary pseudo-destruction to undefined behavior #4944

Open
geryogam opened this issue Sep 27, 2021 · 18 comments · May be fixed by #4953
Open

Update the example of temporary pseudo-destruction to undefined behavior #4944

geryogam opened this issue Sep 27, 2021 · 18 comments · May be fixed by #4953
Assignees
Labels
cwg Issue must be reviewed by CWG.

Comments

@geryogam
Copy link
Contributor

geryogam commented Sep 27, 2021

Since Richard Smith’s proposal P0593R6, a pseudo-destructor call ends the lifetime of a scalar object:

3.6. Pseudo-destructor calls

In the current C++ language rules, "pseudo-destructor" calls may be used in generic code to allow such code to be ambivalent as to whether an object is of class type:

template<typename T> void destroy(T *p) { p->~T(); }

When T is, say, int, the pseudo-destructor expression p->~T() is specified as having no effect. We believe this is an error: such an expression should have a lifetime effect, ending the lifetime of the int object. Likewise, calling a destructor of a class object should always end the lifetime of that object, regardless of whether the destructor is trivial.

This change improves the ability of static and dynamic analysis tools to reason about the lifetimes of C++ objects.

So doesn’t the statement 0 .T::~T(); given in [expr.prim.id.dtor]/3 produce undefined behavior now that it destroys the temporary object 0 twice?

[Example 1:

struct C { };
void f() {
  C * pc = new C;
  using C2 = C;
  pc->C::~C2();     // OK, destroys *pc
  C().C::~C();      // undefined behavior: temporary of type C destroyed twice
  using T = int;
  0 .T::~T();       // OK, no effect
  0.T::~T();        // error: 0.T is a user-defined-floating-point-literal ([lex.ext])
}

end example]

@JohelEGP
Copy link
Contributor

Clang seems to agree, but not the others: https://godbolt.org/z/41q4vszP8.

[basic.life] says

The lifetime of an object o of type T ends when:
(1.3) if T is a non-class type, the object is destroyed, or

From [expr.prim.id.dtor], it seems that only explicitly evaluating 0 .T::~T() destroys the materialized temporary. Where is the implicit destruction that would make this UB specified?

@JohelEGP
Copy link
Contributor

Where is the implicit destruction that would make this UB specified?

That seems to be [class.temporary] p7. So it would seem that GCC and MSVC are non-conforming here.

@xmh0511
Copy link
Contributor

xmh0511 commented Sep 28, 2021

I think that example cannot prove that 0 .T::~T() causes UB. since [expr.ref#3] is not clear about whether the pseudo-destructor is constexpr

If the object expression is of scalar type, E2 shall name the pseudo-destructor of that same type (ignoring cv-qualifications) and E1.E2 is an lvalue of type “function of () returning void”.

[basic.life#7] has no strong evidence/never that says 0 .T::~T() causes UB since the pseudo-destructor is not non-static member function of scalar type.

@geryogam
Copy link
Contributor Author

Let’s ask @zygoloid since he is the author of the proposal P0593R6.

@jensmaurer
Copy link
Member

A few remarks here. First, the comment is certainly wrong, because "no effect" is wrong. We definitely explicitly destroy the 0 temporary here, causing its lifetime to end before the end of the full-expression.

The pseudo-destructor is not a member function, so the question whether it is constexpr doesn't arise (it's handled the way it is by direct core language semantics without involving member functions, even though the syntax looks similar).
Can you point to a provision in [expr.const] that would make invoking a pseudo-destructor non-constexpr? I don't think so.

It seems [basic.life] p7 doesn't speak to the question whether you can end the lifetime of a scalar twice.

@jensmaurer
Copy link
Member

@jensmaurer jensmaurer added the cwg Issue must be reviewed by CWG. label Sep 29, 2021
@xmh0511
Copy link
Contributor

xmh0511 commented Sep 30, 2021

For the first point, [expr.call] p5 says

If the postfix-expression names a destructor or pseudo-destructor ([expr.prim.id.dtor]), the type of the function call expression is void;

Hence, 0 .T::~T(); is a function call. As the example mentioned by @JohelEGP

constexpr void f() {
  using T = int;
  (void)0 .T::~T();
}
int main() {
  []() consteval {
    f();
  }();   // #1 shall be a constant expression as per [expr.const] p13
}

An expression E is a core constant expression unless the evaluation of E, following the rules of the abstract machine ([intro.execution]), would evaluate one of the following:

  • an invocation of a non-constexpr function;

It is not clear whether the pseudo-destructor is a constexpr function or not, hence it's not clear whether #1 is well-formed or not.

For the second point, it is not clear how to destroy an object of scalar type, whether it behaves as if the corresponding pseudo-constructor is called for that object or behaves something else. At least, [basic.lifetime] didn't say such a case is UB. Incidentally, in [class.dtor] p14, should we consider the destructor of a scalar type as the pseudo-destructor since it didn't restrict type X is class type?

@languagelawyer
Copy link
Contributor

@xmh0511 0 .T::~T() is a function call expression, but there is no function invocation.

@xmh0511
Copy link
Contributor

xmh0511 commented Sep 30, 2021

@languagelawyer Since it is a function call expression, why is there no function invocation? What's the difference here?

@languagelawyer
Copy link
Contributor

Since it is a function call expression, why is there no function invocation?

Because there is no function to invoke.

What's the difference here?

Function call is an expression of the form «postfix-expression ( expression-listopt )», function invocation — is transfer of the control flow to a function body. Evaluation of a pseudo-destructor call is described without invocation.

@xmh0511
Copy link
Contributor

xmh0511 commented Sep 30, 2021

@languagelawyer An function call expression can directly cause a function invocation, an function invocation can be implicitly caused. For the following concept

function invocation — is the transfer of the control flow to a function body. Evaluation of a pseudo-destructor call is described without invocation.

I didn't see an explicit definition in the current standard. Assume it is, however, it's unclear whether a pseudo-destructor has a function body or not. Specifically, [basic.def.odr] p7 designates that it's an odr-use of that function; [basic.def.odr] p10 requires that function shall have a definition.

@languagelawyer
Copy link
Contributor

[basic.def.odr] p7 designates that it's an odr-use of that function; [basic.def.odr] p10 requires that function shall have a definition.

I think it should be just s/name/represent/ in [expr.prim.id.dtor] for pseudo-destructor.

@xmh0511
Copy link
Contributor

xmh0511 commented Sep 30, 2021

@languagelawyer However, 0 .T::~T is a function, according to [expr.ref] p3

If the object expression is of scalar type, E2 shall name the pseudo-destructor of that same type (ignoring cv-qualifications) and E1.E2 is an lvalue of type “function of () returning void”.

According to [basic.lval] p1

A glvalue is an expression whose evaluation determines the identity of an object or function.

Hence, we could arguably say 0 .T::~T is an lvalue that denotes a function, hence [basic.def.odr] applies here.

@languagelawyer
Copy link
Contributor

Could be solved by introducing the concept of an «empty lvalue» from CWG232. There are other lvalues, in addition to *(T*)0, which can't denote an object.

@languagelawyer
Copy link
Contributor

Actually, I think [expr.ref]/3 should just be prvalue, like in non-scalar type case.

@xmh0511
Copy link
Contributor

xmh0511 commented Sep 30, 2021

@languagelawyer Agree. I would wonder why is this example not accepted by the implementations

int main() {
   using T = int;
   T a = 0;
   auto ptr = &(a.~T);
}

a.~T is an lvalue, according to [expr.unary.op] p3

The operand of the unary & operator shall be an lvalue of some type T. The result is a prvalue.

  • If the operand is a qualified-id naming a non-static or variant member m of some class C, the result has type “pointer to member of class C of type T” and designates C​::​m.
  • Otherwise, the result has type “pointer to T” and points to the designated object ([intro.memory]) or function ([basic.compound]).

The example should be fine. It seems that empty value cannot solve the vague of pseudo-destructor since &*(int*)0 is well-formed.

@languagelawyer
Copy link
Contributor

@xmh0511
Copy link
Contributor

xmh0511 commented Sep 30, 2021

@languagelawyer Ah, right! I forgot that rule. It seems empty value can work here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cwg Issue must be reviewed by CWG.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants