14

Consider the following builder-like class, which ultimately allows me to construct an object with both certain (runtime) values for member variables, as well as embedding some behavior which is carried by several (compile-time) types.

The same build allows one to update member variables (the usual builder pattern), as well as change template type parameters associated with the type-carried state of the builder (only shown with a couple of template type parameters and members, but in practice, there would be more):

template <typename T1 = DefaultT1, typename T2 = DefaultT2>
class Builder {
  int param1, param2;
  Builder(int param1, int param2) : param1{param1}, param2{param2} {}
public:
  Builder() : Builder(default1, default2) {}

  // methods to change param1 and param2 not shown

  /* return a new Builder with T1 changed to the given T1_NEW */
  template <typename T1_NEW>
  Builder<T1_NEW, T2   > withT1() { return {param1, param2}; }

  template <typename T2_NEW>
  Builder<T1   , T2_NEW> withT2() { return {param1, param2}; }

  Foo make() {
    // uses T1 and T2 to populate members of foo
    return Foo{ typename T1::member, typename T2::another };
  }
};

Note the withT1<> and withT2<> methods which allow you to return a new builder with a different type for T1 or T2 respectively. The bodies for these methods are identical: return {param1, param2};, and in practice much more complicated than shown here (e.g., if there are many parameters).

I'd like to factor the body out into some method which does the construction, like:

template <typename T1_, typename T2_>
Builder<T1_, T2_> copy() { return {param1, param2}; }

and then each withT* method could just call copy.

However, it isn't clear to me how to avoid including the fully qualified type of Builder in the call:

template <typename T1_NEW>
Builder<T1_NEW, T2   > withT1() { return copy<T1_NEW, T2>(); }

Here the cure is worse than the original poison since I need to qualify each copy call with <T1_NEW, T2> (and this is different for each withT* method). Is there some way I can refer to the return type or another type of deduction which I can use to call copy() in the same way in each function?

I'm writing in C++11, but discussion of how a C++11 solution could be improved in later standards is also welcome.

Njeru Cyrus
  • 1,753
  • 21
  • 22
BeeOnRope
  • 60,350
  • 16
  • 207
  • 386
  • How do you plan to use the builder? Do you want to create objects, for example, doing something like `auto builder = make_builder().with().with(); Foo f = builder.make(5, 10.f);`? Assuming that the `Foo` constructor takes an int and a float arg. Or do you want to store the arguments in the builder? Something like `auto builder = make_builder().with(5).with(10.f); Foo f = builder.make();`? – pschill May 26 '18 at 12:36
  • @pschill - yes, more or less, except it only makes on type of object (the object is a template however - the type of the template parameter is passed to the `make()` call). Internally, types tracked by the builder like `T1` and `T2` don't affect the type of the object produced, but are used inside the `make` function e.g., to create function pointers that embed behavior specific to the `T1` and `T2` objects. You can see an example [here](https://gist.github.com/travisdowns/c5645188747cb915997d07790859228c) in `OneshotBuilder`. – BeeOnRope May 28 '18 at 23:19
  • Here is the [current file](https://github.com/travisdowns/uarch-bench/blob/e1d92fb7d54417ecf6092c8e4caddc2556db482c/oneshot.hpp). The idea is to remove the need to have a lot of different `make()` overheads that take different combinations of `TOUCH`, `SAMPLES`, `WARMUP`, etc, and rather embed those in the type of the make, using the `withXXX()` calls to update one of the types and return a new maker with this type but otherwise unchanged. – BeeOnRope May 28 '18 at 23:21
  • Is C++11 a hard restriction? Or are you allowed to use some C++14 features? I have a nice solution, but it requires variadic template arguments. – pschill May 29 '18 at 07:37
  • Unfortunately, yes, it is a hard restriction for me. That said, a solution involving C++14 May certainly be interesting for other people. @pschill – BeeOnRope May 29 '18 at 16:43
  • You could at perhaps use a using declaration to define the withTx return types? Doesn't "solve" the problem, but it breaks up the noise. – Gem Taylor May 30 '18 at 10:23
  • @GemTaylor - can you give an example? The problem is that each of the `with` functions has a _different_ return type: in particular, one template parameter is different (substituted with a new type) in each one. – BeeOnRope May 31 '18 at 19:20
  • I'm just thinking you might be able to declare a `template using Builder1 = Builder' and `template using Builder2 = Builder' etc, just to split off some of the clutter. I'm not sure if you will get away with the cheaty name scoping, though. – Gem Taylor Jun 01 '18 at 09:28

3 Answers3

8

You can introduce a builder proxy that has an implicit conversion to save yourself some typing:

template<typename T1, typename T2>
struct Builder;

struct BuilderProxy
{
    int param1, param2;

    template<typename T1, typename T2>
    operator Builder<T1, T2>() const { return {param1, param2}; }
};

template <typename T1, typename T2>
struct Builder {
    int param1, param2;
    Builder(int param1, int param2) : param1{param1}, param2{param2} {}

    BuilderProxy copy() { return {param1, param2}; }

    template <typename T1_NEW>
    Builder<T1_NEW, T2   > withT1() { return copy(); }

    template <typename T2_NEW>
    Builder<T1   , T2_NEW> withT2() { return copy(); }
};

int main() {
    Builder<int, int> a(1, 2);
    Builder<double, double> b = a.withT1<double>().withT2<double>();
}
Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
8

I dont have a solution for C++11, but as you said yourself, a C++14 may be helpful for others.

If I understood correctly, you need a class that stores arbitrary arguments with a convenient way to pass all arguments to a constructor. This can be achieved using variadic template arguments and std::tuple:

#include <tuple>

template <typename... Args>
class Builder
{
public:
    explicit Builder(Args... args)
        : arg_tuple(std::forward<Args>(args)...)
    {}

    template <typename T>
    T make()
    {
        return std::make_from_tuple<T>(arg_tuple);
    }

    template <typename T>
    Builder<Args..., T> with(T t)
    {
        return std::make_from_tuple<Builder<Args..., T>>(std::tuple_cat(arg_tuple, std::make_tuple(std::move(t))));
    }

private:
    std::tuple<Args...> arg_tuple;
};

template <typename... Args>
Builder<Args...> make_builder(Args... args)
{
    return Builder<Args...>(std::forward<Args>(args)...);
}

Usage:

struct Foo
{
    Foo(int x, int y)
        : x(x), y(y)
    {}
    int x;
    int y;
};

struct Bar
{
    Bar(int x, int y, float a)
        : x(x), y(y), a(a)
    {}
    int x;
    int y;
    float a;
};

int main()
{
    auto b = make_builder().with(5).with(6);
    auto foo = b.make<Foo>();  // Returns Foo(5, 6).
    auto b2 = b.with(10.f);
    auto bar = b2.make<Bar>();  // Returns Bar(5, 6, 10.f).
}

While std::make_from_tuple is C++17, it can be implemented using C++14 features:

namespace detail
{
    template <typename T, typename Tuple, std::size_t... I>
    constexpr T make_from_tuple_impl(Tuple&& t, std::index_sequence<I...>)
    {
        return T(std::get<I>(std::forward<Tuple>(t))...);
    }
}

template <typename T, typename Tuple>
constexpr T make_from_tuple(Tuple&& t)
{
    return detail::make_from_tuple_impl<T>(
        std::forward<Tuple>(t),
        std::make_index_sequence<std::tuple_size_v<std::remove_reference_t<Tuple>>>{});
}
pschill
  • 5,055
  • 1
  • 21
  • 42
  • Thanks. This is quite generic and perhaps not exactly applicable to my use case. In my case, the `make()` method treats various template parameters of the `Builder` object specially. For example `T1` may be a non-type function parameter, and the `make()` method may take the function pointer and include into the object to be made. Similarly `T2` may be treated in a different way. All that to say that the builder is not generic and the various template parameters (type and non-type) are not homogenous. This a good solution for the general homogeneous case, however! – BeeOnRope May 31 '18 at 03:14
2

Not sure I understood your problem 100%. Let me try a tentative answer anyway: I added to your code snippet a templated implicit conversion operator which is calling a reinterpret_cast

template<typename U1, typename U2>
operator Builder<U1, U2>(){ return *reinterpret_cast<Builder<U1, U2>*>(this); }

This is generally hacky and unsafe, but in your case it does the job. it allows the withT1 and withT2 member functions to be like

template <typename T1_NEW>
Builder<T1_NEW, T2   > withT1() { return *this; }

template <typename T2_NEW>
Builder<T1   , T2_NEW> withT2() { return *this; }

the snippet I used for testing the code is attached below

#include <type_traits>

template<typename T1, typename T2>
struct Foo;

template<>
struct Foo<int, double>{
    int mInt;
    double mDouble;
};

template<>
struct Foo<char, double>{
    char mChar;
    double mDouble;
};

struct t1{
    using type = int;
    static type member;
};

struct t2{
    using type = double;
    static type another;
};

struct tt1{
    using type = char;
    static type member;
};

template <typename T1 = t1, typename T2 = t2>
class Builder {
  // int param1, param2;
  Builder(int param1, int param2) : param1{param1}, param2{param2} {}
public:
  int param1, param2;
  Builder() : Builder(0, 0) {}

  template<typename U1, typename U2>
  operator Builder<U1, U2>(){ return *reinterpret_cast<Builder<U1, U2>*>(this); }

  template <typename T1_NEW>
  Builder<T1_NEW, T2   > withT1() { return *this; }

  template <typename T2_NEW>
  Builder<T1   , T2_NEW> withT2() { return *this; }

  Foo<typename T1::type, typename T2::type> make() {
    // uses T1 and T2 to populate members of foo
    return Foo<typename T1::type, typename T2::type>{T1::member, T2::another};
  }
};


int main(){
Builder<t1, t2> b;
auto c = b.withT1<tt1>();
static_assert(std::is_same<decltype(c), Builder<tt1, t2>>::value, "error");
}
Paolo Crosetto
  • 1,038
  • 7
  • 17