Search code examples
c++11unique-ptrraiiboost-optional

Indirect Member RAII: unique_ptr or optional?


Consider a class with a member that can't be stored directly, e.g., because it does not have a default constructor, and the enclosing class's constructor doesn't have enough information to create it:

class Foo
{
public:
    Foo(){} // Default ctor
private:
    /* Won't build: no default ctor or way to call it's 
        non-default ctor at Foo's ctor. */
    Bar m_bar; 
};

Clearly, m_bar needs to be stored differently, e.g., through a pointer. A std::unique_ptr seems better, though, as it will destruct it automatically:

    std::unique_ptr<Bar> m_bar;

It's also possible to use std::experimental::optional, though:

    std::experimenatl::optional<Bar> m_bar;

My questions are: 1. What are the tradeoffs? and 2. Does it make sense to build a class automating the choice between them?

Specifically, looking at the exception guarantees for the ctor of std::unique_ptr and the exception guarantees for the ctor of std::experimental::optional, it seems clear that the former must perform dynamic allocation and deallocation - runtime speed disadvantages, and the latter stores things in some (aligned) memory buffer - size disadvantages. Are these the only tradeoffs?

If these are indeed the tradeoffs, and given that both types share enough of their interface (ctor, operator*), does it make sense to automate the choice between them with something like

 template<typename T>
 using indirect_raii = typename std::conditional<
     // 20 - arbitrary constant
     sizeof(std::experimental::optional<T>) > 
         20 + sizeof(std::exerimental::optional<T>)sizeof(std::unique_ptr<T>),
     std::unique_ptr<T>,
     std::experimental::optional<T>>::type;

(Note: there is a question discussing the tradeoffs between these two as return types, but the question and answers focus on what each conveys to the callers of the function, which is irrelevant for these private members.)


Solution

  • IMO there are other trade-offs at play here:

    • unique_ptr is not copyable or copy-assignable, while optional is.
      I suppose one thing you could do is make indirect_RAII a class-type and conditionally add definitions to make it copyable by calling Bar's copy ctor, even when unique_ptr is selected. (Or conversely, disable copying when it's an optional.)
    • optional types can have a constexpr constructor -- you can't really do the equivalent thing with a unique_ptr at compile-time.
    • Bar can be incomplete at the time that unique_ptr<Bar> is constructed. It cannot be incomplete at the time that optional<Bar> is known. In your example I guess you assume that Bar is complete since you take its size, but potentially you might want to implement a class using indirect_RAII where this isn't the case.
    • Even in cases where Bar is large, you still may find that e.g. std::vector<Foo> will perform better when optional is selected than when unique_ptr is. I would expect this to happen in cases where the vector is populated once, and then iterated over many times.

    It may be that as a general rule of thumb, your size rule is good for common use in your program, but I guess for "common use" it doesn't really matter which one you pick. An alternative to using your indirect_RAII type is, just pick one or the other in each case, and in places where you would have taken advantage of the "generic interface", pass the type as a template parameter when necessary. And in performance-critical areas, make the appropriate choice manually.