Callee-destroy versus caller-destroy parameter lifetimes

I was just discussing Clang’s [[trivial_abi]] attribute with Mathias Stearn. Specifically, the fact that [[trivial_abi]] changes the Itanium ABI for trivial-ABI types so that parameters of trivial-ABI type are owned and destroyed by the callee, rather than by the caller.

Now consider two pieces of C++ code. One of them works in practice today; the other does not. Neither one is guaranteed to work. They both have implementation-defined behavior (that is, it is implementation-defined whether their behavior is undefined or not).

Here’s the first snippet. Defined or undefined?

const std::string& foo(std::unique_ptr<std::string> ptr) {
    return *ptr;
    // the controlled object will still be valid in our caller
}

int main() {
    std::cout << foo(std::make_unique<std::string>("defined or undefined?")) << "\n";
}

Here’s the second snippet. Defined or undefined?

std::unique_lock<std::mutex> with(std::mutex& m) {
    return std::unique_lock<std::mutex>(m);
}

bool sink(std::unique_lock<std::mutex> lk) {
    return true;
    // the lock is automatically released at the end of this scope
}

std::mutex m;
int main() {
    return sink(with(m)) && sink(with(m));
}

The first snippet compiles and runs with no issues. It is implementation-defined as correct on the Itanium ABI!

The second snippet compiles clean, but results in a double-lock of m and deadlock. It is implementation-defined as undefined behavior on the Itanium ABI!

Yes, really implementation-defined. [expr.call] sentence 9.7:

It is implementation-defined whether the lifetime of a parameter ends when the function in which it is defined returns or at the end of the enclosing full-expression.

On Visual Studio, using Microsoft’s calling convention, the second snippet compiles and runs clean — the unique_lock object is destroyed in the callee and there is no deadlock.

And, as you might now expect, on Visual Studio the first snippet produces undefined behavior because the unique_ptr is destroyed in the callee and the string freed before the caller gets a chance to print it out!


As described in my blog post “[[trivial_abi]] 101” (2018-05-02), one side effect of the [[clang::trivial_abi]] attribute is that it turns caller-destroy types into callee-destroy types — which can turn working code into non-working code, or vice versa, in cases such as the above two snippets.

The solution for you in practice, of course, is never to write tricky code like the above! Here are the guidelines:

  • Don’t ever return a reference that depends on the lifetime of a local variable (as done in our first snippet). GCC and Clang will even give a warning if you return a reference to a parameter; although they won’t warn about our unique_ptr example due to its complexity.

  • Don’t rely on deterministic destruction of parameter objects. If you need deterministic destruction, move the parameter into a non-parameter variable. This rule would have saved our second snippet. However…

  • Don’t ever perform two concurrency-related operations in the same statement! Our original sin in the second snippet was that we wrote with(m) twice on the same line. We should have written something more like

      bool result = sink(with(m));
      if (result) {
          result = sink(with(m));
      }
      return result;
    

I admit I can’t come up with a super great rewrite of the second snippet, because it’s so contrived. But contrived code can still teach us a lesson. How sure are you that your codebase doesn’t contain any instances of either of these pitfalls?

Posted 2018-11-12