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

Bind const lvalue reference to a temporary of a class type #496

Open
h0nzZik opened this issue Jun 25, 2019 · 3 comments
Open

Bind const lvalue reference to a temporary of a class type #496

h0nzZik opened this issue Jun 25, 2019 · 3 comments

Comments

@h0nzZik
Copy link
Contributor

h0nzZik commented Jun 25, 2019

Suppose we have a class A and a function foo returning A by value:

struct A{};
A foo(){return A();}

Then an expression that consists only of a call to foo() is a prvalue. As in the followin snippet, an attempt to bind that prvalue to a const lvalue reference (or to an rvalue reference?) should result in a temporary materialization, according to dcl.init.ref/5.3, and the reference should be bound to the temporary. (The temporary materialization includes copy-constructing the class A.

int main() {
  A const &a = foo();
}

In that case, the execution semantics get stuck at bindReference3, where a side condition requires the initializer to have a non-class type:

bindReference3(
	lvcpp(loc(obj(#token("3","Int"),#token("4","Int"),auto(#token("0","Int"))),#token("0","Int")),`noTrace_CPP-TRACE-SYNTAX`(.KList),tcpp(quals(`.Set`(.KList)),`.Set`(.KList),lvRefType(tcpp(quals(`SetItem`(`Const`(.KList))),`.Set`(.KList),classType(classId(`GlobalNamespace`(.KList),classSpecifier(`Struct`(.KList),`Identifier`(#token("\"A\"","String")),`.List{"_,__CPP-DYNAMIC-OTHER-SORTS"}`(.KList)))))))),
	prv(loc(obj(#token("4","Int"),#token("1","Int"),auto(#token("0","Int"))),#token("0","Int")),`noTrace_CPP-TRACE-SYNTAX`(.KList),tcpp(quals(`.Set`(.KList)),`.Set`(.KList),classType(classId(`GlobalNamespace`(.KList),classSpecifier(`Struct`(.KList),`Identifier`(#token("\"A\"","String")),`.List{"_,__CPP-DYNAMIC-OTHER-SORTS"}`(.KList))))))
)

For non-class types as in

int const &b = 3;

everything works.

The same problematic situation occurs when the reference binding is done as a part of a function call:

void bar(A const & p){}
int main() {
  bar(foo());
}

However, if the parameter of function bar is unnamed, the problem disappears (and the copy constructor of A is called).

void bar(A const &){}
@h0nzZik
Copy link
Contributor Author

h0nzZik commented Jul 3, 2019

I would like some code for reference binding, namely bindReference3, to be evaluated in translation semantics, so that we may copy-construct a temporary of a class type, or do conversions. The only thing that should happen in execution semantics is the actual binding (we may also rename bindReference4 to bindReferenceExec).

One way to do so is to do figureInit in bindParams even for reference types. The catch, however, is that the code that does the actual binding in execution semantics needs the resulting initializer to evaluate to the reference, i.e. to an lv(...) of a reference type. However, an lvalue of a reference type is rewritten to an lvalue of the referenced object (of nonreference type), which gets into addToExecEnv and causes troubles.

One way to solve the problem would be to make sure that the reference result of reference binding does not rewrite to the object being referred. We could for example wrap it in a newly created referendeBindingResult that would be a KResult, and then unwrap it again when needed in bindParam.

Another way would be to create a new reference referring to the same object as the rewritten reference does, using declareNonStaticObjectExec with bindReference as we do now. However, bindReference would need to be changed to bindReferenceExec. A drawback is that this way, two references would be created instead of one.

@h0nzZik
Copy link
Contributor Author

h0nzZik commented Jul 29, 2019

After #522, the above code still does not work - but now it gets stuck in the translation semantics, in makePRVal, while processing the function A foo(){return A();}. Is it a bug in the referenced PR? EDIT: no, it fails the same with the current master.

EDIT2: It fails when processing a call to the copy-constructor, in reference binding, when strict-evaluating the RHS - a FunctionalCast.

Also, I can not reproduce the situation from June 25, even with version from that time.

@h0nzZik
Copy link
Contributor Author

h0nzZik commented Aug 1, 2019

According to n4296 12.2/5, a temporary bound to a reference lives either to the end of the full-expression (if bound to a function parameter in function call expression, to returned value in return statement, or a to new-initializer), or to the end of life of the reference (in other cases; I can think only of a declaration of a reference).

How do we destroy non-lifetime-extended (i.e. those that are destroyed at the end of full-expression) temporaries? We might collect them in a cell and then use the fullExpression term to do some cleanup. Or we might do a source transformation as in this SO answer. How would that transformation look like? Every full-expression would be transformed into a compound-statement (a block). But what would we do with an expression that is a part of a variable declaration? Unlike in Rust, C++ blocks are not expressions and do not yield values, so we would need to extend them. So I would prefer the first option.

Q: what happens with temporaries now? I can think of the following cases:

A();
A(), B(); // comma operator
foo(A());

How do we destroy lifetime-extended temporaries? We already deal with those temporaries that are created as a part of reference binding - see the rule for bindReferenceToTemporary. Imho, this can happen only when the initializer is a non-class non-array prvalue. The other situation is when the reference is initialized with a temporary object.

Currently, destructors of global objects are not called

TODOs

  • destruct local temporaries
  • destruct global variables

I can see two ways how can we extend the lifetime of a temporary that is bound to a reference. Either we change the semantics of temp(...) so that it pushes into locally-constructed when we detect it bounds to a reference, or we replace temp(...) with declareObject - similarly to here.

Which C++ constructs produce temporary? Those constructs might create an expression of the shape prv(temporary(TheTemporaryExpr, InitializerExpr), _, _). bindReference would then match it and marked TheTemporaryExpr to be lifetime-extended. In the execution semantics, the term temporary(TheTemporaryExpr, InitializerExpr) would first evaluate TheTemporaryExpr which would add it to the locally-constructed cell, and then it would evaluate the initializer expression. If we evaluated InitializerExpr only, the effect would be similar, except that the evaluation would work with the original TheTemporaryExpr that would not be marked as lifetime-extended.
Would this work for C++14 or C++17?

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0135r0.html says:

We conclude that a prvalue expression of class or array type should not create a temporary object. Instead, the temporary object is created by the context where the expression appears, if it is necessary. The contexts that require a temporary object to be created ("materialized") are as follows:

  • when a prvalue is bound to a reference
  • when member access is performed on a class prvalue
  • when array subscripting is performed on an array prvalue
  • when an array prvalue is decayed to a pointer
  • when a derived-to-base conversion os performed on a class prvalue
  • when a prvalue is used as a discarded value expression

So while in C++14 bindReference need to handle the situation when the reference is bound to a temporary, in C++17 it is directly bindReference - the context - which should create the temporary. However, it first needs to evaluate the initializer, at least partially, in order to know whether it is a prvalue.

TODOs

  • Where in the C++17/C++14 standard temporaries are materialized/created?
  • Where we create temporaries in the semantics?
  • What in the standard is a prvalue/xvalue?

prvalues:

  • literals (all except string literal)
  • result of an implicit conversion to non-reference type (n4296 4/6)
  • this (n4296 5.1.1/3)
  • id-expression, unless it names a function, variable, or data member (n4296 5.1.1/8)
  • lambda-expression
  • function-call of non-reference type
  • explicit type conversion - T(x1, x2, ...), T()
  • E.member_function
  • x++, x--
  • dynamic_cast to a pointer type
  • static_cast and reinterpret_cast to a non-reference type, some const_casts
  • and more

Temporary materialization

  • array-to-pointer conversion (n4727 7.2/1)
  • a prvalue is a discarded value expression (n4727 8.2.3/2)
  • typeid(prvalue) (n4727 8.5.1.8/3)
  • const_cast<>(prvalue) (n4727 8.5.1.11/4)
  • sizeof(prvalue) (n4727 8.5.2.3/4)

what is "temporary expression"?

Clang-kast also generates TemporaryObjectExpr from CXXTemporaryObjectExpr and MaterializeTemporaryExpr (here), first of which we strip and second we rewrite to FunctionalCast.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant