1

Is there a way of mimicking Kotlin's data class 'copy()' in C++ ?
This allows copying the object and modify specific parts of it within a single expression, see Kotlin example:

data class SomeData(val a: Int, val b: String)
...
SomeData(a = 10, b = "10").copy(b = "ten")

In C++20 I can do

struct SomeData {
    int a;
    string b;
};
...
SomeData sd1 = { .a = 10, .b = "10" };
SomeData sd2 = { .a = sd1.a, .b = "ten" };

but this gets very clunky and unsafe when structs get larger and larger.

I am considering writing my own function that would behave as a copy constructor, but would also take an std::initializer_list to apply the designated initialization over the copied object.

  1. Is there any language support for this I could take advantage or I am left with my own function only ?
    note: I am unable to use C++23
  2. Any suggestions for my own function implementation ?

1 Answer 1

2

Immediately invoked lambda

One alternative that connects data member names with the value to be changes are using an immediately invoked lambda:

auto sd2 = [](auto s) { s.a = 12; s.b = 'ten'; return s; }(sd1);

A macro could potentially be used to make this into a simpler form.

Variadics and pointer-to-members

Another approach that I was hoping would end up less clunky (but didn't) are to use variadic auto non-type template parameters with pointer-to-members as arguments, to specify which members of a modified copy that you would like to replace along with their new values:

namespace detail {

// Helper trait to get underlying type of member from pointer-to-member.
template <typename T> struct ptr_to_member_underlying;

template <typename ClassType, typename MemberType>
struct ptr_to_member_underlying<MemberType ClassType::*> {
  using type = MemberType;
};

template <typename T>
using ptr_to_member_underlying_t = typename ptr_to_member_underlying<T>::type;

template <typename T, auto mem_ref, typename V>
void assignMember(T &t, V &&mem_value) {
  t.*mem_ref = std::forward<V>(mem_value);
}

} // namespace detail

/// @tparam mem_refs        Pointer-to-members for the public data members of
///                         `t`that should be modified in the copy.
/// @param  t               Object to be copied.
/// @param  new_mem_values  New values for the modified data members of `t`.
template <auto... mem_refs, typename T>
T modifiedCopy(T const &t,
               detail::ptr_to_member_underlying_t<decltype(mem_refs)>
                   &&... new_mem_values) {
  auto tc = t;
  (detail::assignMember<T, mem_refs>(
       tc, std::forward<detail::ptr_to_member_underlying_t<decltype(mem_refs)>>(
               new_mem_values)),
   ...);
  return tc;
}

int main() {
  SomeData sd1{.a = 10, .b = "10"};
  auto sd2 = modifiedCopy<&SomeData::b>(sd1, "ten");
}

This also emphasizes "specify the members you want to change" semantics as opposed to the one you list which have "specify the members you want to keep and change" semantics. This differentiation could help when structs are becoming bigger, in case they are generally more similar than dissimilar (still the concern of separating member reference to the value that should be assigned to it).

You could use a macro to make this less clunky, however with the general concerns of using macros.

Useful third party libs?

boost_pfr could be used to create a briefer (tuple-like referencing) modifiedCopy API that takes data member indices as non-type template parameters instead of pointers-to-members:

auto sd2 = modifiedCopy<1>(sd1, "ten");
                     // ^-- modify member 1 (2nd member; 0-based indexing) to "ten" 
Sign up to request clarification or add additional context in comments.

3 Comments

I love the fact this is very type-safe. However this is still far from ideal as you are unable to modify more than one field at once. For now I am using a higher-order function that takes a function from the caller and allows for multiple modification so you would get something like auto b = copy_(a, [](A& a) { a.field1 = ...; a.field2 = ...; }); There are couple of downsides of this so I am thinking now about the combination of this and the solution of yours. Thanks for the very advanced answer btw. I can learn much from this
@foo A higher-order function like that may actually have the best semantics, as it connects the field to change to the value to change it to. Note that my approach above does allow modifying arbitrarily many fields at once (the variadics: auto sd2 = modifiedCopy<&SomeData::a, &SomeData::b>(sd1, 42, "ten");) but it suffers from the "disconnect" between field name and value change, which is a risk. I think macros could help to semantically re-connect field references with values, but it comes this other challenges.
@foo An alternative to the higher order function would be an immediately invoked lambda: auto sd2 = [](auto s) { s.a = 12; s.b = 'ten'; return s; }(sd1);, where potentially a macro can be used to expand to such immediately invoked lambda.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.