How to use copy elision when function is called in if block

Viewed 155

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

3 Answers

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();

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

A a = [&] {
  if (…) {
    return function1();
  } else {
    return function2();
  }
}();

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.)

Related