0

I followed this article to implement a class which has a private constructor and a static "make instance" function. The function returns a std::optional object depending on some condition, and will call the constructor only if the condition is met, otherwise makeInstance returns an nullopt.

The error I got is:

error: no matching function for call to 'make_optional'
            return make_optional<Engine1>({move(p)});
                   ^~~~~~~~~~~~~~~~~~~~~~
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/13.0.1/../../../../include/c++/13.0.1/optional:1448:5: note: candidate function template not viable: cannot convert initializer list argument to 'Engine1'
    make_optional(_Tp&& __t)
    ^
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/13.0.1/../../../../include/c++/13.0.1/optional:1456:5: note: candidate template ignored: substitution failure [with _Tp = Engine1]: deduced incomplete pack <(no value)> for template parameter '_Args'
    make_optional(_Args&&... __args)
    ^
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/13.0.1/../../../../include/c++/13.0.1/optional:1464:5: note: candidate template ignored: requirement 'is_constructible_v<Engine1, std::initializer_list<std::unique_ptr<int, std::default_delete<int>>> &>' was not satisfied [with _Tp = Engine1, _Up = typename std::remove_reference<unique_ptr<int> &>::type, _Args = <>]
    make_optional(initializer_list<_Up> __il, _Args&&... __args)
    ^

from

#include <iostream>
#include <optional>
#include <memory>
using namespace std;

class Engine1
{
private:
    explicit Engine1(unique_ptr<int> p): ptr(move(p)) {};
    unique_ptr<int> ptr;
public:
    static optional<Engine1> makeInstance()
    {
        auto p = make_unique<int>(123);
        bool success = true;

        if (success)
            return make_optional<Engine1>({move(p)});
        else
            return {};
    }
};

int main()
{
    auto ins = Engine1::makeInstance();
    return 0;
}

I suspect it has something to do with the unique_ptr member so I tried using a raw pointer.

class Engine2
{
private:
    explicit Engine2(int *p): ptr(p) {};
    int *ptr;
public:
    static optional<Engine2> makeInstance()
    {
        auto p = new int(123);
        bool success = true;

        if (success)
            return make_optional<Engine2>(p);
        else
            return {};
    }
};

in this case I got a similar error:

error: no matching function for call to 'make_optional'
            return make_optional<Engine2>(p);
                   ^~~~~~~~~~~~~~~~~~~~~~
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/13.0.1/../../../../include/c++/13.0.1/optional:1448:5: note: candidate function template not viable: no known conversion from 'int *' to 'Engine2' for 1st argument
    make_optional(_Tp&& __t)
    ^
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/13.0.1/../../../../include/c++/13.0.1/optional:1456:5: note: candidate template ignored: requirement 'is_constructible_v<Engine2, int *&>' was not satisfied [with _Tp = Engine2, _Args = <int *&>]
    make_optional(_Args&&... __args)
    ^
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/13.0.1/../../../../include/c++/13.0.1/optional:1464:5: note: candidate template ignored: could not match 'initializer_list<_Up>' against 'int *'
    make_optional(initializer_list<_Up> __il, _Args&&... __args)
    ^

but after moving the constructor to public, the code could compile.

class Engine3
{
private:
    int *ptr;
public:
    explicit Engine3(int *p): ptr(p) {};
    static optional<Engine3> makeInstance()
    {
        auto p = new int(123);
        bool success = true;

        if (success)
            return make_optional<Engine3>(p);
        else
            return {};
    }
};

This doesn't happen to the unique_ptr though. My questions are:

  1. what are the problems with Engine1 and Engine2
  2. why and how does make_optional treat unique_ptr and raw pointer differently also, could someone please provide a complete example using the error handling method described in the article since the original post doesn't contain one. Thanks!
Alan Birtles
  • 32,622
  • 4
  • 31
  • 60
user8822312
  • 255
  • 4
  • 14
  • A quick fix is to not mark the ctor as explicit – joergbrech Feb 25 '23 at 09:06
  • 1
    Well, you've made constructor `explicit` so you should create class instance explicitly: `return make_optional(Engine1{move(p)});` or (better) `return optional{Engine1{move(p)}};` or (even better) `return ::std::optional{Engine1{::std::move(p)}};` @AlanBirtles The problem is not caused by constructor being `private` (`make_optional` does not use it) so this is not a duplicate. – user7860670 Feb 25 '23 at 09:12
  • @user7860670 you should make that an answer – joergbrech Feb 25 '23 at 11:38

1 Answers1

0

The std::make_optional has 3 signatures:

  • one that "creates an optional object from value"
  • two that "creates an optional object constructed in-place"

In the first case, compiler doesn't understand that the initializer list is an Engine. In the second case, there is no constructor of Engine with initializer list.

The solution is to construct Engine explicitly: return make_optional<Engine1>(Engine1 {move(p)});.

Engine2 differs, cause you don't use curly brackets in std::make_optional<Engine2>(p);, so the compiler tries to use in-place construction, but constructor is private. Engine3 makes it public. That's why everything works.

Finally, I don't know what is your constraints, but in my opinion in most cases throwing from the constructor is the proper way to handle exceptional situations. It makes your code simpler increasing the maintainability. Also it's possibly violates the principle of single responsibility as the class itself manages the conditions of it's existence (:

Maybe you should create interfaces for Engine and its factory, which will check the fulfillment of conditions and create instances.

  • `make_optional` does not try to access any private constructors here. – user7860670 Feb 25 '23 at 10:21
  • @user7860670, could you clarify what it does then? According to cppreference `std::make_optional` has 3 signatures: one that "Creates an optional object from value" and two that "Creates an optional object constructed in-place". In this question the second case is presented, so this is equivalent to `return std::optional(std::in_place, std::forward(args)...);`. How can this object be created if no constructor is called. – Georgij Krajnyukov Feb 25 '23 at 11:11
  • *in this question the second case is presented* - nope, `Engine1` instance is passed into `make_optional` by value, then `make_optional` uses (implicitly generated public) copy / move constructor. Making all constructors public won't solve the issue. – user7860670 Feb 25 '23 at 11:18
  • @user7860670 Yeah, I see now. Thanks for replying! Made a correction to my answer. – Georgij Krajnyukov Feb 25 '23 at 11:44
  • what's the reason that `Engine3` does compile whereas `Engine2` doesn't? – user8822312 Feb 25 '23 at 13:57
  • @user8822312, sorry, I missed this moment. In `Engine2` you use the second signature (for in-place construction), cause you don't use curly brackets in `std::make_optional(p);`. That means that compiler tries to use the constructor, but it's private. `Engine3` has public constructor, so it's okay. – Georgij Krajnyukov Feb 25 '23 at 14:38