1

(I don't understand the root cause yet so the title may not be correct.)

I have the following c++ code which does not compile:

#include <iostream>
#include <memory>
#include <vector>
#include <variant>
#include <optional>

template <typename T>
class A {
 public:
  ~A() = default;
  std::unique_ptr<int> ptr = nullptr;
};

template <typename T>
class B : public A<std::vector<T>> {
 public:
  static B<T> Create() {
    return B<T>();
  }
};

using AllTypes = std::variant<B<int64_t>, B<uint64_t>, B<double>>;

std::optional<AllTypes> Get() {
  // Error! No viable conversion!
  return B<int64_t>::Create();
}

The error message is:

main.cpp: In function ‘std::optional<std::variant<B<long int>, B<long unsigned int>, B<double> > > Get()’:
main.cpp:34:28: error: could not convert ‘B::Create() [with T = long int]()’ from ‘B’ to ‘std::optional, B, B > >’
   34 |   return B<int64_t>::Create();
      |          ~~~~~~~~~~~~~~~~~~^~
      |                            |
      |                            B<long int>

The error can be reproduced here: https://onlinegdb.com/2H-K9x0kq.

but if I remove the unique ptr variable ptr, it can correctly compile. And if I move the ptr member to the child class B it also compiles.

Can anyone give some hints on what is wrong? Right now I don't know what keywords/rules to search for. Many thanks!

Update:

  1. I changed the original absl::StatusOr to std::optional instead. I feel the errors might be because of the same problem.
Patrick
  • 555
  • 1
  • 6
  • 25
  • 1
    @273K The `use of undeclared identifier 'std'` might be because your code has a default `using namespace std;`? – Patrick Jul 08 '23 at 23:21
  • My code? It was exact your code. – 273K Jul 08 '23 at 23:28
  • @JaMiT Thanks for pointing it out! I changed the `absl::StatusOr` to `std::optional`. It's not exact my use case but it seems to be giving a similar error. (I also updated the error message accordingly) – Patrick Jul 08 '23 at 23:36
  • Remove user defined default `~A()`. See https://stackoverflow.com/questions/56968443/explicitly-defaulted-destructor-disables-default-move-constructor-in-a-class – 273K Jul 08 '23 at 23:42
  • @273K Thanks! It does make the error go away. Can you help clarify why it solves the problem? And I plan to add virtual functions to class A, so eventually I think I need to have a `virtual ~A() = default` in class A. Anyway to keep the definition? – Patrick Jul 08 '23 at 23:49
  • See https://stackoverflow.com/questions/24342941/what-are-the-rules-for-automatic-generation-of-move-operations – 273K Jul 08 '23 at 23:49
  • Oh just saw your updated link. Will take a look thanks! – Patrick Jul 08 '23 at 23:50

1 Answers1

5

Let me start by simplifying your example. I'm guessing there was already significant simplification, but there are more layers that could have been pulled off. In particular, B is unnecessary, as is wrapping the variant in another template.

#include <memory>
#include <variant>

class A {
 public:
  virtual ~A() = default; // will be used as a base class
  std::unique_ptr<int> ptr = nullptr;
};

std::variant<A, int> Get() {
  // Error! No viable conversion!
  return A();
}

As you reported, this works if the unique_ptr is removed. What does this tell me? One of the properties of uniqur_ptr is that it cannot be copied; sometimes this property triggers construction errors.

Another piece of the puzzle is that declaring a destructor suppresses the move constructor. The net result is that your class A can be neither copied (because of the unique_ptr) nor moved (because of the user-declared destructor). So another way to present this situation is the following:

#include <variant>

class A {
 public:
  A() = default;        // <-- must be explicitly declared now
  A(const A&) = delete; // <-- Was deleted because of the `unique_ptr`
  A(A&&) = delete;      // <-- Was deleted because of the user-declared destructor
};

std::variant<A, int> Get() {
  // Error! No viable conversion!
  // Can neither copy nor move this temporary object into a `variant`.
  return A();
}

One fix is to remove the user-declared destructor. However, since there is inheritance involved, this is not a good choice. Another fix is to declare the move constructor (and move assignment, if that is needed).

#include <memory>
#include <variant>

class A {
 public:
  virtual ~A() = default; // will be used as a base class
  A() = default;
  A(A&&) = default; // <-- Allow move construction

  std::unique_ptr<int> ptr = nullptr;
};

std::variant<A, int> Get() {
  // No error!
  return A();
}
JaMiT
  • 14,422
  • 4
  • 15
  • 31
  • Thank you so much for the detailed answer! Didn't realize B was not even needed to reproduce this error :) Thanks again! – Patrick Jul 09 '23 at 01:17