How to best DRY with some ad-hoc class instances?

80 Views Asked by At

Sometimes in my code, I find myself wanting to define several instances of the same class, with the difference in the definition being small relative to its overall length, and sharing some common designator. And - I struggle to refactor the code so that it's both compact and easy to use.

Example: I start with:

auto blue_foo = std::make_unique<float[]>(n);
auto blue_bar = std::make_unique<float[]>(m);
auto blue_baz = std::make_unique<float[]>(k);

This is kind of icky: Repetition of commands and name prefixes... :-(

I can put this these variables in a struct named blue, so that I could write

do_stuff_with(blue.foo, blue.baz, 123);

rather than

do_stuff_with(blue_foo, blue_baz, 123);

but - then I would need even more repetition:

struct {
    std::unique_ptr<float[]> foo, bar, baz;
} blue = {
    std::make_unique<float[]>(n),
    std::make_unique<float[]>(m),
    std::make_unique<float[]>(k),
};

So wasteful! I would have at least liked to write something like:

struct { auto foo, bar, baz; } foo = {
    std::make_unique<float[]>(n),
    std::make_unique<float[]>(m),
    std::make_unique<float[]>(k),
};

but that's not possible.

Also, I don't want to repeat the make_unique call. I could write a lambda, I suppose:

auto make_data = [](std::size_t size) { return std::make_unique<float[]>(size); }

struct { auto foo, bar, baz; } blue = { make_data(n), make_data(m), make_data(k) };

but the lambda is a lot of writing; and it doesn't save all that much. And even that has some repetition.

What is an idiomatic approach for maiing my initial piece of code compact and readable?

Note: In the example, the code snippet is not repeated many times; just once. If I repeatedly used a combination of foo, bar and baz I would write a class for holding them.

4

There are 4 best solutions below

3
user12002570 On

You can make a helper function that returns std::tuple of unique_ptr and then use structure bindings. The general idea is as follows:

auto make(float n, float m, float k)
{ 
 return std::tuple{std::make_unique<float[]>(n), std::make_unique<float[]>(m), std::make_unique<float[]>(k)};
} 
    
auto [blue_A, blue_B, blue_C] = make(n,m, k);

0
einpoklum On

@NathanOliver makes the interesting suggestion of using a dictionary / std::map. But that still makes me repeat a bunch of code. If we have a let with an appropriate transformer of key-ctor-arguments pairs to key-contructed-values pairs, and if we could instantiate it withput any template ambiguity issues, that might let us write:

dict<std::make_unique<float[]>> blue { 
    { "foo", n }, 
    { "bar", m },
    { "baz", k }
};

do_stuff_with(blue["foo"], blue["baz"], 123);

Benefits:

  • No repetition of anything; specifically, we only need a single templated definition of dict for all such cases in all programs.
  • No artificial prefixing
  • Can pass all of blue together

Detriments:

  • Haven't actually implemented this to establish it works...
  • More mental load on reader of this code
  • Bespoke class, not in the standard library
  • Performance overhead in the code that uses it
3
joergbrech On

The shortest (but not necessarily most readible) version that I could come up with is using a variadic template immediately invoked lambda:

#include <memory>

int main() {
    auto blue = [](auto... ns) {
        struct { 
            std::unique_ptr<float[]> foo, bar, baz; 
        } ret { std::make_unique<float[]>(ns)... };
        return ret;
    }(1, 2, 3);
}

https://godbolt.org/z/9c8cvc8oW

It doesn't have any repetitions, but I am not sure you would call it idiomatic.

You can simplify this and improve readibility if you are ok with using new instead of std::make_unique and repeating new float[x] three times:

#include <memory>

int main() {
    struct { 
        std::unique_ptr<float[]> 
            foo{new float[1]}, 
            bar{new float[2]}, 
            baz{new float[3]}; 
    } blue;
}
0
Jan Schultke On

In your specific example, all members have the same type, which makes std::array a viable option:

using array_type = float[];
auto [foo, bar, baz] = std::array{
    std::make_unique<array_type>(n),
    std::make_unique<array_type>(m),
    std::make_unique<array_type>(k),
};

This eliminates most of the repetition, but std::make_unique still gets called thrice. For smaller examples, you could solve this as follows:

constexpr auto make = [](std::size_t size) { return std::make_unique<float[]>(size); };
auto [foo, bar, baz] = std::array{make(n), make(m), make(k)};

For larger examples where even calling make multiple times is a problem, you can write something like:

// reusable code
template <typename T, std::size_t N, typename F>
auto transform(std::array<T, N> data, F f)
  -> std::array<decltype(f(data[0])), N>
{
    return std::apply([&f](auto&... x) -> decltype(transform<T, N, F>(data, f)) {
        return { f(x)... };
    }, data);
}

// one-time code
constexpr  auto make = [](std::size_t size) { return std::make_unique<float[]>(size); };
auto [foo, bar, baz] = transform(std::array{n, m, k}, make);

You can go as far as obtaining the syntax transform({n, m, k}, make); if you don't use std::array, but a T(&data)[N] parameter, similar to std::to_array.