Which compiler is correct for this subtle ternary operator code example, gcc or clang?

174 Views Asked by At

Consider the following code:

#include <unordered_map>
#include <iostream>

template<template<class, class, class...> class Map, typename Key, typename T, typename KeyP, typename... Args>
const T& getMapEntry(const Map<Key, T, Args...>& map, const KeyP& key, const T& defval) {
    static_assert(std::is_convertible_v<KeyP, Key>);
    auto i = map.find(key);
    return i != map.end() ? i->second : defval;
}

template<template<class, class, class...> class Map, typename Key, typename T, typename KeyP, typename... Args>
T getMapEntry(const Map<Key, T, Args...>& map, const KeyP& key, T&& defval) {
    static_assert(std::is_convertible_v<KeyP, Key>);
    auto i = map.find(key);
    return i != map.end() ? i->second : std::move(defval);
}

template<template<class, class, class...> class Map, typename Key, typename T, typename KeyP, typename... Args>
T getMapEntry(const Map<Key, T, Args...>& map, const KeyP& key, const T&& defval) = delete;

class Foo {
public:
    Foo() { std::cout << __PRETTY_FUNCTION__ << '\n'; }
    ~Foo() { std::cout << __PRETTY_FUNCTION__ << '\n'; }
    Foo(const Foo&) { std::cout << __PRETTY_FUNCTION__ << '\n'; }
    Foo& operator=(const Foo&) { std::cout << __PRETTY_FUNCTION__ << '\n'; return *this; }
    Foo(Foo&&) { std::cout << __PRETTY_FUNCTION__ << '\n'; }
    Foo& operator=(Foo&&) { std::cout << __PRETTY_FUNCTION__ << '\n'; return *this; }
};

int main() {
    std::unordered_map<int, Foo> map;
    auto&& ret = getMapEntry(map, 123U, Foo{});
    return 0;
}

gcc outputs:

Foo::Foo()
Foo::Foo(Foo&&)
Foo::~Foo()
Foo::~Foo()

clang outputs:

Foo::Foo()
Foo::Foo(const Foo &)
Foo::~Foo()
Foo::~Foo()

Using an if/else chain always invokes the move ctor. Using the following line, which is more revealing, also causes the move ctor to be invoked:

return i != map.end() ? T{i->second} : std::move(defval);

Which implementation is correct?

1

There are 1 best solutions below

3
AndyG On BEST ANSWER

The question surrounds what happens during:

  • evaluation of a ternary, and
  • possible copy elision

The expression:

return i != map.end() ? i->second : std::move(defval);

per [expr.cond]

Will attempt to find an implicit conversion sequence between i->second and std::move(defval) (both directions).

  • i->second (expression 1, or E1) is an lvalue
  • std::move(defval) (expression 2 or E2) is an xvalue

[expr.cond 4.2] states:

If E2 is an xvalue, the target type is “rvalue reference to T2”, but an implicit conversion sequence can only be formed if the reference would bind directly.

And this almost applies; we'd allow an lvalue to rvalue conversion only if the lvalue could be bound without invoking a copy. Unfortunately this rule cannot be used becaue i->second cannot be converted to an rvalue without a copy, so we fall back to [4.3.3] which states:

[...] otherwise, the target type is the type that E2 would have after applying the lvalue-to-rvalue, array-to-pointer, and function-to-pointer standard conversions.

Meaning the return type will be a prvalue, and we finally have to deal with lvalue-to-rvalue conversion of i->second, which necessitates a copy, per [conv.lval] (an xvalue is also a glvalue, which allows this conversion to apply)

Otherwise, if T has a class type, the conversion copy-initializes the result object from the glvalue.

So we're ultimately returning an prvalue, which has mandatory copy-elision. This is why there is not yet-another constructor call logged to the console.

Therefore the correct behavior in this case is to see a call to the copy-constructor of Foo, and clang is correct.

If you want to enforce that move construction happens in your case, you cannot use a ternary expression:

    if (i != map.end())
       return i->second;
    else
        return std::move(defval);

This will ensure that both compilers call the move constructor of Foo if the else branch is invoked.