4
\$\begingroup\$

This java answer made me wonder whether it would be possible to implement the builder pattern in C++ in a way that checks at compile-time whether all necessary members of the constructed object are actually set before the object is constructed. This is what I came up with:

struct Foo {
    int x_;
    int y_;
    int z_;
};

template <bool x_set = 0, bool y_set = 0, bool z_set = 0>
struct FooBuilder {
    int x_, y_, z_;

    Foo build() {
        static_assert(x_set && y_set && z_set, "all members must be initialized before building a Foo!");
        return { x_, y_, z_};
    }

    FooBuilder<1,y_set,z_set> x(int val) {
        FooBuilder<1,y_set,z_set> result;
        result.x_ = val;
        result.y_ = y_;
        result.z_ = z_;
        return result;
    }

    FooBuilder<x_set,1,z_set> y(int val) {
        FooBuilder<x_set,1,z_set> result;
        result.x_ = val;
        result.y_ = y_;
        result.z_ = z_;
        return result;
    }

    FooBuilder<x_set,y_set,1> z(int val) {
        FooBuilder<x_set,y_set,1> result;
        // optional check to allow z to be set only once
        static_assert(!z_set, "z may not be initialized twice!"); 
        result.x_ = val;
        result.y_ = y_;
        result.z_ = z_;
        return result;
    }
};

int main() {
    FooBuilder<> b;
    auto f1 = b.x(1).y(2).z(3).build();
    auto f1a = b.z(3).y(2).x(1).build();
    auto f2 = b.x(1).y(2).build(); // fails with static assertion 
    auto f3 = b.x(1).y(2).z(3).z(4).build(); // fails with static assertion 
    auto partially_initialized_builder = b.x(1);
    auto f4 = partially_initialized_builder.build(); // fails...
    auto f5 = partially_initialized_builder.y(4).z(6).build(); // runs

    return 0;
}

With this approach, I like the fact that programming errors that result from forgotten arguments to the builder can be caught at compile-time. However, I'm not sure if it is too verbose, since the whole content of the builder has to be copied/moved in each setter method (even if the copies might be optimized away).

So my main questions are:

  1. Would such a pattern actually be useful in practice?
  2. Is there an elegant way of sharing the member variables of the Builder objects, despite the results of each setter call being of a different type? (For cheap-to-move types, I am thinking along the lines of a "data block"-like struct, that gets moved from this into the result.)
  3. Any other downsides to this approach?

P.S.: Please disregard the lack of access control (x_ etc. should be private), this question is more about the high-level picture ;-)

\$\endgroup\$
3
\$\begingroup\$

Would such a pattern actually be useful in practice?

Not really in my opinion. In C++20 you can use designated initializer lists like {.min = 10, .max = 20} and I mean if the argument is not clear from the context than there are various ways to improve this, even today.

  1. A comment. Foo f(/*min=*/10);
  2. User-defined literals Rectangle r(10_width, 200_height);
  3. Strong types: Player p(Position(10, 10));
  4. Variables: const int size = 10; Square s(size);

This also doesn't work with non-copyable, non-moveable types, although those are rare in practice. The members also need to be default constructible unfortunately.

Some thoughts:

  1. Note that your code has undefined behavior, since two of the variables are not set on the first call.

  2. The build function is required in Java, but in C++ you can use a conversion operator. Although this suffers from the same problem that expression templates have: Using type deduction will not get you a Foo.

  3. Delete copy construction for the builder object, since there is no reason to copy it.

  4. Don't use 0 and 1 for bools please; false and true is clearer IMO.

  5. Consider using noexcept and constexpr (if applicable).

Is there an elegant way of sharing the member variables of the Builder objects, despite the results of each setter call being of a different type? (For cheap-to-move types, I am thinking along the lines of a "data block"-like struct, that gets moved from this into the result.)

Yes that's a good idea. Here's my take on it:

template <bool x_set = false, bool y_set = false, bool z_set = false>
struct FooBuilder {
  constexpr FooBuilder() noexcept = default;
  FooBuilder(const FooBuilder &) = delete;
  FooBuilder &operator=(const FooBuilder &) = delete;

  constexpr auto x(int val) noexcept {
    members_.x_ = val;
    return FooBuilder<true, y_set, z_set>(std::move(*this));
  }
  constexpr auto y(int val) noexcept {
    members_.y_ = val;
    return FooBuilder<x_set, true, z_set>(std::move(*this));
  }
  constexpr auto z(int val) noexcept {
    static_assert(!z_set, "cannot set z twice!");
    members_.z_ = val;
    return FooBuilder<x_set, y_set, true>(std::move(*this));
  }

  constexpr operator Foo() noexcept {
    static_assert(x_set && y_set && z_set, "all members must be set");
    return {members_.x_, members_.y_, members_.z_};
  }
  constexpr Foo build() noexcept { return *this; }

private:
  template <bool... args>
  constexpr FooBuilder(FooBuilder<args...> &&other) noexcept
      : members_{std::move(other.members_.x_), std::move(other.members_.y_),
                 std::move(other.members_.z_)} {}

  template <bool, bool, bool> friend struct FooBuilder;

  struct Proxy {
    int x_, y_, z_;
  } members_{};
};

There is still a lot of boilerplate, but unfortunately it cannot be removed since C++ doesn't have reflection yet.

\$\endgroup\$
4
  • \$\begingroup\$ It's also possible to write a macro to generate such a builder automagically :) \$\endgroup\$
    – Rakete1111
    Feb 4 '19 at 14:33
  • 1
    \$\begingroup\$ Thanks for your insights - especially the one about the C++20 designated initializer lists. They are almost exactly the feature that I wanted to emulate. Also, the variadic move constructor really reduces the amount of boilerplate code, great idea! (And the points about the copy constructor, 0/1, uninitialized variables are valid, I'll excuse that with me being a bit sloppy while writing the code ;) ) \$\endgroup\$
    – anderas
    Feb 4 '19 at 14:37
  • \$\begingroup\$ @Rakete1111, have you thought about tuple of std::optionals? I thought about using string literals as template arguments to create struct with named variables, but then recalled that I cannot name a member of a struct with compile time string. It would be great if one could overload operator-> to return one of those optionals properly named, and at the build phase check if all have been initialized. It will of course place constraints of being moveable and destructible, but I don't think it is too much to ask. \$\endgroup\$ Feb 4 '19 at 18:05
  • \$\begingroup\$ @Incomputable We need reflection like yesterday :) \$\endgroup\$
    – Rakete1111
    Feb 4 '19 at 18:38

Not the answer you're looking for? Browse other questions tagged or ask your own question.