10

Hi stackoverflow community,

I'm a few months into C++ and recently I've been trying to grasp the concepts revolving around the "new" value categories, move semantics, and especially temporary materialization.

First of all, it's not straightforward to me how to interpret the term "temporary materialization conversion". The conversion part is clear to me (prvalue -> xvalue). But how exactly is a "temporary" defined in this context? I used to think that temporaries were unnamed objects that only exist - from a language point of view - until the last step in the evaluation of the expression they were created in. But this conception doesn't seem to match what temporaries actually seem to be in the broader context of temporary materialization, the new value categories, etc.

The lack of clarity about the term "temporary" results in me not being able to tell if a "temporary materialization" is a temporary that gets materialized or a materialization that is temporary. I assume it's the former, but I'm not sure. Also: Is the term temporary only used for class types?

This directly brings me to the next point of confusion: What roles do prvalues and xvalues play regarding temporaries? Suppose I have an rvalue expression that needs to be evaluated in such a way that it has to be converted into an xvalue, e.g. by performing member access. What will exactly happen? Is the the prvalue something that is actually existent (in memory or elsewhere) and is the prvalue already the temporary? Now, the "temporary materialization conversion" described as "A prvalue of any complete type T can be converted to an xvalue of the same type T. This conversion initializes a temporary object of type T from the prvalue by evaluating the prvalue with the temporary object as its result object, and produces an xvalue denoting the temporary object" at cppreference.com (https://en.cppreference.com/w/cpp/language/implicit_conversion) converts the prvalue into an xvalue. This extract makes me think that a prvalue is something that is not existent anywhere in memory or a register up until it gets "materialized" by such conversion. (Also, I'm not sure if a temporary object is the same as a temporary.) So, as far as I understand, this conversion is done by the evaluation of the prvalue expression which has "real" object as a result. This object is then REPRESENTED (= denoted?) by the xvalue expression. What happens in memory? Where has the rvalue been, where is the xvalue now?

My next question is more specific question about a certain part of temporary materialization. In the talk "Understanding value categories in C++" by Kris van Rens on YouTube (https://www.youtube.com/watch?v=liAnuOfc66o&t=3576s) at ~56:30 he shows this slide:

Understanding value categories in C++ by Kris van Rens

Based on what cppreference.com says about temporary materialization numbers 1 and 2 are clear cases (1: member access on a class pravlue, 2: binding a reference to a prvalue (as in the std::string +operator).

I'm not too sure about number 3, though. Cppreference says: "Note that temporary materialization does not occur when initializing an object from a prvalue of the same type (by direct-initialization or copy-initialization): such object is initialized directly from the initializer. This ensures "guaranteed copy elision"." The +operator returns a prvalue. Now, this prvalue of type std::string is used to initialize an auto (which should resolve to std::string as well) variable. This sounds like the case that is discussed in the prior cppreference excerpt. So does temporary materialization really occur here? And what happens to the objects (1 and 2) that were "denoted" by the xvalue expressions in between? When do they get destroyed? And if the +operator is returning an prvalue, does it even "exist" somewhere? And how is the object auto x " initialized directly from the initializer" (the prvalue) if the prvalue is not even a real (materialized?) object?

In the talk "Nothing is better than copy or move - Roger Orr [ACCU 2018]" on YouTube (https://www.youtube.com/watch?v=-dc5vqt2tgA&t=2557s) at ~ 40:00 there is nearly the same example:

Nothing is better than copy or move

This slide even says that temporary materialization occurs when initializing a variable which clearly contradicts the exception from cppference from above. So what's true?

As you can see, I'm pretty confused about this whole topic. For me, it's especially hard to grasp these concepts as I cannot find any clear definitions of various term that are used in a uniform way online. I'd appreciate any help a lot!

Best regards, Ruperrrt

TL;DR: What is a temporary in the temporary materialization conversion context? Does that mean that a temporary gets materialized or that it is materialization that is temporary? Also temporary = temporary object?

In the slides, is 3 (first slide) respectively 1 (second slide) really a point where temporary materialization occurs (conflicts with what cppreference says about initialization from pravlues of the same type)?

Ruperrrt
  • 489
  • 2
  • 13

2 Answers2

8

107 views, 6 months and no answer nor comments. Interesting. Here's my take on your question.

Temporary materialization should mean "temporary that gets materialized".

The term temporary is not only used for class types.

Prvalues, loosely speaking, don't exist in memory unlike xvalues. The thing you should care about is the context. Let's say you have defined a structure struct S { int m; };.

In the expression S x = S();, subexpression S() denotes a prvalue. The compiler with treat it just as if you have written S x{}; (note that I've put curly brackets on purpose because S x(); is actually a declaration of a function). On the other hand in expression like int i = S().m;, subexpression S() is a prvalue that will be converted to xvalue that is, S() will denote something that will exist in the memory.

Regarding your second question, the thing you need to know about is that with the C++-17, the circumstances in which temporaries are going to be created were brought down to minimum (cppreference describes it very well). However, the expression

auto x = std::string("Guaca") + std::string("mole").c_str();

will require two temporary objects to be created before assignment. Firstly, you are doing member access with c_str() method so a temporary std::string will be created. Secondly, the operator + will bind one a reference to the std::string("Guaca") (new temporary). and one to the result object of c_str(), but without creating additional temporary because of:. That's pretty much it. It's worth to note that the order of creation temporary objects isn't known - it totally depends on the compiler.

After that, we're calling the operator + which probably constructs another std::string which technically isn't a temporary object because that's a part of the implementation. That object might or might not be constructed into the memory location of x depending on the NRVO. In any case, whatever value does the prvalue expression std::string("Guaca") + std::string("mole").c_str() denote will be the same value (of the same object) denoted by the expression x because of cpp.ref:

Note that temporary materialization does not occur when initializing an object from a prvalue of the same type (by direct-initialization or copy-initialization): such object is initialized directly from the initializer. This ensures "guaranteed copy elision".

This quote isn't really precise and might confuse you so I also suggest reading copy elision (the first part about mandatory elision).

I'm not a C++ expert so take all of this with a grain of salt.

domdrag
  • 541
  • 1
  • 4
  • 13
  • For my own conscience, I need to comment that the last part of the answer (where I talk about the object being denoted by the expression) isn't correct. I basically stated that prvalue (RHS) denotes an object which cannot be true. Prvalues never denote objects. If there isn't NRVO involved, the `std::string`, that the `operator +` returns, would be constructed into the place where the variable `x` can retrieve it to call its move-constructor and by that, avoid the creation of an additional (temporary) object as in pre C++17. For details, I recommend reading the posts in my second comment. – domdrag Mar 18 '23 at 09:36
  • For further details, [Why does it use the term "object" here when mentioning a prvalue?](https://stackoverflow.com/questions/49462233/why-does-it-use-the-term-object-here-when-mentioning-a-prvalue) and [Result of a prvalue](https://stackoverflow.com/questions/53909077/result-of-a-prvalue). – domdrag Mar 18 '23 at 09:37
  • *"It's worth to note that the order of creation temporary objects isn't known - it's totally implementation-defined."* — could you please elaborate on that? I believe in your example the creation order is unspecified (because order of evaluation of function arguments is unspecified (operator+)). I found no specific rule for the order of temporary objects creation. – cppbest Apr 14 '23 at 15:00
  • @cppbest That's exactly what I meant, probably my wording wasn't precise enough. By saying "*it's totally implementation-defined*", I meant to say that the creation order is unspecified because the order of function arguments evaluation isn't specified in the Standard - just as you stated. – domdrag Apr 14 '23 at 18:24
  • OK, just want to note that unspecified behaviour is slightly different from implementation-defined one – cppbest May 12 '23 at 04:51
  • @cppbest That's true, it's been fixed now. – domdrag May 12 '23 at 14:27
2

The accepted answer by domdrag is great. As your question is highly faceted I want to add some primitive understanding I have gained from studying C++ templates: The Complete Guide, specifically section Appendix B: B.2.1 Temporary Materialization (p.676). Hopefully this is helpful in some way.

Disclaimer: I am not an expert either :) , please season appropriately.


Temporary materialization removes the need for some compiler copy/move elision by minimising the creation of temporary objects. This is a new language feature and can be illustrated giving the following example:

class N{
public:
    N(){};

    // delete copy & move constructors
    N(const N& other) = delete;
    N(const N&& other) = delete;
};

N make_N(){
    // Create first temporary (t1) (pre C++17)
    return N{};
}

int main(){
    // Construct second temporary (t2) (pre C++17)
    // Copy/move second temporary (t2) into N using the '=' operator (pre C++17).
    auto n = make_N();
    return 0;
}

Pre C++17:

  1. The function make_N() would construct a temporary (t1) within its scope.
  2. This temporary (t1) would then be copied/moved into another temporary (t2) within the main() scope.
  3. Finally, the '=' operator would build 'n' using 't2' via copy or move construction. All of this temporary business (t1, t2) would be elided (RVO) by any decent compiler resulting in make_N() constructing a single object directly within 'n'. However, this elision was somewhat optional, so the compiler must also demand that copy & move constructors exist, just in-case. The above code does not compile with any pre C++17 compiler.

Post C++17:

  1. make_N() creates an object of type 'N' within 'n'. Avoiding the excessive use of temporary objects is now a language feature and the reliance on compiler optimisation is removed. The above code does compile with a post C++17 compiler.

TL;DR: Prior to C++17 the use of temporary objects was excessive and the standard relied on compilers to optimize through elision. Post C++17 the use of temporaries is minimised meaning we're less reliant on compiler optimization.