3

(This question is inspired by Nicolai Josuttis' CppCon 2017 talk.)

Consider the following source file (for an object, not a complete program):

#include <string>

class C {
    std::string s_;
public:
    C(std::string s) : s_(s) { };
    void bar();
};

void foo() {
    std::string hello { "The quick brown fox jumped over the lazy dog" };
    C c { hello };
    c.bar();
}

And its compilation result on GodBolt.

Even with the -O2 (and even with -O3) it seems a string constructor is called three times. Specifically, s is constructed, used only to construct s_, then destructed. My questions:

  • Is the compiler allowed to simply construct s_ from the arguments to the ctor, not constructing s at all?
  • If not, is the compiler allowed to move-construct s_ from s, seeing how the latter is unused?
  • If any of the previous answers is "yes" - why aren't gcc and clang doing so?
  • If s is properly constructed, can't the compiler avoid the construction of hello, seeing how it has no other use? Or at least move from it?
einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • You need `-std=c++14` with clang 5.0. – Holt Feb 07 '18 at 14:33
  • 1
    Try to formulate a fool-proof pseudo-algorithm that does the optimization you want. It's harder than you might expect. If you ever get done express it in C++, extensively test it with hopefully all edge cases and then send clang a pull-request. – nwp Feb 07 '18 at 14:35
  • @nwp: I didn't say "this optimization should happen, why isn't it?" ... I don't know the answer. – einpoklum Feb 07 '18 at 14:37
  • I remember them only recently specifying that memory allocation is not a side-effect, so depending on the standard you use compilers might not be allowed to do that optimization. I forgot which version changed that. – nwp Feb 07 '18 at 14:37
  • @nwp memory allocation is not observable behavior of the abstract machine if all uses of that memory are traced (and you can do link time analysis to ensure that nobody injected a non-default operator new on you)? – Yakk - Adam Nevraumont Feb 07 '18 at 14:38
  • @nwp: Your first sentence seems to contradict your second statement. If it _were_ a recognized side-effect then maybe it's necessary to keep it. – einpoklum Feb 07 '18 at 14:40
  • @einpoklum Well, those contradictions are a good reason I put them as comments and not as answers. I feel that even though at least one statement is at least partially incorrect they are still points worth looking into. – nwp Feb 07 '18 at 14:44

1 Answers1

9

Under as-if, I'm certain most of what you ask for could be done, assuming you go to link time and you make bar empty and never override new anywhere.

But then, under as-if, your program is an empty program, it has no observable effects.

The compiler is not permitted to move construct s_ from s under the rules of the abstract machine. If you want it to be move constructed, std::move it.

The situations where an lvalue can be treated as an rvalue are limited and specific and involve return x; statements. This is not a return x; statement.

So your code must copy s into s_. Quite possibly it should also generate a warning as a quality of implementation issue.

The compiler is not permitted to elide s into s_. There have been some proposals to permit much more aggressive elision rules.

But as of right now, elision is only permitted under as-if, with prvalues, or with return x; statements. As-if elimination is really, really hard to prove with something as complex as allocation, most compilers don't try. And it isn't possible at object file generation, because someone could replace the global allocator.

Imagine a global allocator override that prints out how many allocations are done. Then those "never used" objects are used in that they should print out the allocations they do.

Or a global allocator that calls exit after 2 allocations. The abtract machine resulting should never call bar(); if we eliminate your extra objects, the program doesn't behave as the standard mandates.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Under as-if, the object file is not empty, since you don't know what `C::bar()` does. – einpoklum Feb 07 '18 at 14:38
  • @einpoklum Sure; but neither is allocation, because you could have overridden the global new allocator. Anyhow, edited to reflect that. – Yakk - Adam Nevraumont Feb 07 '18 at 14:39
  • I'm not sure what you mean. Aren't the global new and delete part of the standard library? They aren't overridden in the source... and are you saying that the pathology of overriding global new is what prevents the optimizations here? – einpoklum Feb 07 '18 at 14:43
  • @einpoklum The possibility that someone overrides global new/delete means that calls to new/delete cannot be eliminated under "as-if" until link time. Elision can eliminate the creation of objects and does not rely on "as-if", which is the usual way that this optimization can occur. Your cases are not (as of [tag:C++17]) valid elision cases. – Yakk - Adam Nevraumont Feb 07 '18 at 14:45