I mostly work on system-level C++ projects that don't allow exceptions to be thrown, but RAII is (rightfully) strongly encouraged. Right now, we handle the lack of failing constructors using infamous tricks many C++ programmers are familiar with, like:
- Trivial constructor followed by a call to
bool init(Args...)to do the hard stuff - Real constructor followed by checking
bool is_valid() const - Heap-allocating with
static unique_ptr<MyType> create(Args...)
Of course, these all have drawbacks (heap allocation, invalid and "moved" states, etc).
My company is finally updating compilers and will allow glorious C++17 to be used. Since C++17 features std::optional<T> and, most importantly, mandatory copy elision, I was hoping I could greatly simplify all our classes into something that would look like this:
class MyType {
public:
static std::optional<MyType> create() {
// If any of the hard stuff fails, return std::nullopt
return std::optional<MyType>(std::in_place, 5, 'c');
}
~MyType() {
// Cleanup mArg0 and mArg1, which are always valid if the object exists
}
// ... class functionality ...
// Disable default constructor, move, and copy.
// None of these are needed because mandatory copy elision
// allows the static function above to return rvalue without
// copy or move operations
MyType() = delete;
MyType(const MyType&) = delete;
MyType(MyType&&) = delete;
MyType& operator=(const MyType&) = delete;
MyType& operator=(MyType&&) = delete;
private:
MyType(ArgT0 arg0, ArgT1 arg1) : mArg0(arg0), mArg1(arg1) {}
ArgT0 mArg0;
ArgT1 mArg1;
};
Notice how nice this is: Static function ensures all the hard stuff is done before the object is ever created, lack of default ctor/move means object never exists in an invalid or moved state, private constructor ensures user can't accidentally skip the named ctor.
Unfortunately, because the ctor is private, the std::is_constructable_t<MyType> check fails and therefore the in_place constructor of optional is SFINAE'd out.
This code works if I do one of 2 things, neither of which I want to:
- Make the ctor public (But now users of the class can accidentally circumvent the named ctor)
- Allow the move operations (But now I have to deal with invalidated objects)
I have also tried this, but it doesn't work because std::optional required a move operator for this to work:
static std::optional<MyType> create() {
// If any of the hard stuff fails, return std::nullopt
return std::optional<MyType>(MyType(5, 'c'));
}
Is there some trick or incantation I may be missing to get this to work, or have I hit the limits of what C++17 will allow?
Thanks!