4

In the following snippet, no move and no copy of A happens thanks to copy elision

struct A;
A function1();
A function2();

int main(int argc, char**) {
  if (argc > 3) {
    A a = function1();
  } else {
    A a = function2();
  }
  return 0;
}

This is nice, however a is not accessible outside the if-block. When declaring a outside, then a move happens

struct A;
A function1();
A function2();

int main(int argc, char**) {
  A a;
  if (argc > 3) {
    a = function1();
  } else {
    a = function2();
  }
  return 0;
}

What is a recommendable attern to profit from copy elision when it should happen in an if block on the call site into a variable outside the if scope?

Compiler-Exlorer link

Ruslan
  • 18,162
  • 8
  • 67
  • 136
pseyfert
  • 3,263
  • 3
  • 21
  • 47

3 Answers3

7

In this particular case you can use the ternary conditional:

A a = argc>3 ? function1() : function2();

In more complicated cases you may need to save the condition and do several checks, e.g.

const bool cond = argc>3;
A a = cond ? function1() : function2();
A b = cond ? function3() : function4();
Ruslan
  • 18,162
  • 8
  • 67
  • 136
6

Immediately-Invoked Lambda Expression (IILE) can save the day in this and more complicated cases:

A a = [&] {
  if (…) {
    return function1();
  } else {
    return function2();
  }
}();
Anton3
  • 577
  • 4
  • 14
  • Unfortunately, even more complicated scenarios like `if(...) { A a; doSomething(); return a; } else ...` don't get advantage of mandatory copy elision. Moreover, GCC (up to at least 10.1) doesn't even do voluntary copy elision: https://gcc.godbolt.org/z/fGrPUJ (although Clang does, even in version 6.0) – Ruslan Jul 05 '20 at 07:52
  • FWIW, I've reported this to GCC: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=96065 – Ruslan Jul 05 '20 at 08:21
  • Hopefully, this problem will be gone in C++23: http://wg21.link/p2025 – Anton3 Jul 05 '20 at 13:14
2

In general, you have to use placement new to use prvalues (“mandatory copy elision”, which is not copy elision) in arbitrary contexts (e.g., with statements or with reuse of variables). You then also have the responsibility to call the destructor manually; the safe and clean way to do that is to write a helper class:

template<class T>
struct box {
  char buf[sizeof(T)];  // real code should handle alignment
  T *p{};  // will point to buf
  void reset() {
    if(p) p->~T();
    p=nullptr;
  }
  ~box() {reset();}
};

void f() {
  box<A> a;
  if(…) a.p=new (a.buf) A(function1());
  else a.p=new (a.buf) A(function2());
  // use *a.p
}

Of course, box is just a reimplementation of std::optional with the guts exposed. It’s unfortunate that you have to assign to box::p externally, but wrapping the new in a function would of course materialize a temporary for the A returned from whatever function. (Having box::p be a pointer rather than just a flag avoids having to use std::launder to deal with lifetime issues.)

Davis Herring
  • 36,443
  • 4
  • 48
  • 76
  • If you're going to advocate placement `new`, you might want to add a note about cleanup (i.e., manually calling the dtor). – Stephen Newell Jul 04 '20 at 23:58
  • @StephenNewell: I did better than that: I provided a destructor for `box` that does so because I couldn’t stand to write exception-unsafe code that did it manually (or an ugly `catch(...)`). – Davis Herring Jul 05 '20 at 02:11
  • 1
    Yes, but I think an explanation as to why you're manually calling the dtor is useful. I've found this is a major point of confusion since a stunning number of a C++ developers haven't used placement new and think it's never correct to manually call a dtor. – Stephen Newell Jul 05 '20 at 02:17
  • 1
    @StephenNewell: Fair enough: I’ll edit in some admonishment. – Davis Herring Jul 05 '20 at 02:18
  • @StephenNewell I hope most C++ developers aren't using placement new. As ugly as it is, replacing all your ifs with ternaries or IILE is 100x better than this. – Ryan McCampbell Jun 09 '23 at 19:10