Home > Software design >  Move sematics in functions could have a lot of options | Is there a cleaner way? C
Move sematics in functions could have a lot of options | Is there a cleaner way? C

Time:09-26

Move sematics in functions could have a lot of options | Is there a cleaner way? C

Say we have a function append with key and value as parameters. Then I currently would have to define 4 functions to enable move sematics. So for two parameters this is still doable. Though sometimes a function that requires move sematics has a lot more parameters, therefore too much different functions to keep the code maintainable.

Is there a cleaner way to achieve move sematics, perhaps using some form of templates? Or with variadic templates?

Example function append with 2 parameters.

constexpr
auto&       append(
    const Key&      key,
    const Value&    value
) {
    ...
}
constexpr
auto&       append(
    Key&&           key,
    const Value&    value
) {
    ...
}
constexpr
auto&       append(
    const Key&      key,
    Value&&         value
) {
    ...
}
constexpr
auto&       append(
    Key&&           key,
    Value&&         value
) {
    ...
}

This would get a little out of hand for a function with 6 parameters that all require move sematics.

Any solutions?

CodePudding user response:

Yes, there are forwarding references that can accept both lvalues and rvalues. Something like this:

#include <concepts>
#include <utility>

template <typename K, typename V>
struct A
{
    template <std::convertible_to<K> A, std::convertible_to<V> B>
    void append(A &&a, B &&b)
    {
        K key = std::forward<A>(a);
        V value = std::forward<B>(b);
    }
};

std::forward then acts as a conditional move, moving the argument only if an rvalue was received.


But rather than accepting exactly two arguments, I would mimic try_emplace() and insert_or_assign() from standard containers:

#include <concepts>
#include <utility>

template <typename K, typename V>
struct A
{
    template <typename A = K, typename ...B>
    requires std::constructible_from<K, A> && std::constructible_from<V, B...>
    void append(K &&a, B &&... b)
    {
        K key(std::forward<A>(a));
        V value(std::forward<B>(b)...);
    }
};

Now, they use two overloads (for the first parameter being const T & and T &&), but that seems pointless, except for allowing braced lists to be passed to the first argument, which can also be achieved by adding = K as the default template argument.


I would bother with the above only if the function needs to be optimal, e.g. if you're writing your own container.

In less demanding places, I'd do as @user17732522 suggests and pass by value, then std::move(). This incurs one extra move compared to a reference (either a forwarding one, or 2N overloads as in the question), which should be good enough.

CodePudding user response:

You can simply take the arguments by-value and then std::move them into the container's storage. This may incur one extra move construction if a glvalue of the same type is passed as argument, but will otherwise cost the same in terms of constructor calls as overloading on both lvalues and rvalues would. The move construction should normally be cheap and can often be optimized away entirely.

constexpr
auto&       append(
    Key      key,
    Value    value
) {
    // when constructing the container element use `std::move(key) and `std::move(value)`
}
  • Related