Search code examples
c++implicit-conversioninitializer-list

Return value is copied when using initializer list with a user-defined conversion operator that returns reference


I'm trying to write a wrapper around shared_ptr that can implicitly dereference to underlying type. The code is as below:

#include <memory>

template<typename T>
class PtrWrapper {
public:
  PtrWrapper(std::shared_ptr<T> ptr) : ptr_(ptr) {}
  operator T& () {
    return *ptr_;
  }
  T& ref() {
    return *ptr_;
  }
private:
  std::shared_ptr<T> ptr_;
};

Looks like that there's nothing wrong with it. I tried a few methods to use the wrapper:

#include <iostream>

class Nothing {
public:
  Nothing() {
    std::cout << "Construct " << this << std::endl;
  }
  Nothing(Nothing const& parent) {
    std::cout << "Copy " << &parent << " " << this << std::endl;
  }
  Nothing(Nothing && parent) {
    std::cout << "Move " << &parent << " " << this << std::endl;
  }
  ~Nothing() {
    std::cout << "Destruct " << this << std::endl;
  }
};

int main() {

  PtrWrapper<Nothing> wrapper{std::make_shared<Nothing>()};

  // #1: OK
  Nothing & by_assignment = wrapper;
  // #2: OK
  Nothing & by_operator{wrapper.operator Nothing &()};
  // #3: OK
  Nothing & by_function{wrapper.ref()};
  // #4: OK
  Nothing & by_initialization(wrapper);

  // #5: Compile error: non-const lvalue reference to type 'Nothing' cannot bind to an initializer list temporary
  // Nothing & by_initialization_2{wrapper};

  // #6: The `Nothing` class is copied, which is not expected
  Nothing const& by_initialization_3{wrapper};

  return 0;
}

The wrapper class works well with assignment and parentheses initialization.

The weird thing is, when I'm trying to initialize Nothing& with initializer list (#5 and #6 in the code above), the value is copied and I must use a const reference to it. However, when I explicitly call the conversion operator like wrapper.operator Nothing &() (#2 in code above), I got the correct reference to the original object constructed at the first line.

I've read cppreference and found that initializer list is a copy-initialized temporary list, but it doesn't make sense why the code works when operator Nothing &() is explicitly called.

Anyone can help me with figuring out what is happening here? Thanks very much!


Solution

  • You are actually doing reference initialization here:

    Nothing & by_initialization_2{wrapper};
    

    The rules say that since the initializer is not the same type as the reference being bound, user-defined conversion operators are considered, which is fine, since you have the appropriate conversion operator.

    However, if the l-value returned by the conversion function is passed through a brace-init list, then a temporary is materialized. Since you can't bind a non-const reference to a temporary, the initialization fails.