7

I have a following header:

#include <string_view>
#include <tuple>

std::tuple<int, int, int> foo(std::string_view sv);

class s {
    public:
    s(std::string_view sv);

    private:
    int x, y, z;
};

Is there any way to implement s::s so that it assigns x, y, z using foo in initializer list? Assume that foo is expensive so should not be called multiple times, and due to interdependencies between the returned values, it cannot be broken down either.

Without using initializer list, this task is trivial:

s::s(std::string_view sv) {
    std::tie(x, y, z) = foo(sv);
}

But it would not work if x, y, z were types without default constructor. One solution I did find is delegating to a private constructor:

s::s(std::string_view sv) : s{ foo(sv) } {}

s::s(const std::tuple<int, int, int>& t) :
    x{ std::get<0>(t) },
    y{ std::get<1>(t) }, 
    z{ std::get<2>(t) }
{}

However I find it a bit unelegant as it requires modifying the header with an implementation detail. Are there any other solutions to this problem?

2 Answers 2

7

You cannot directly initialize x, y and z at the same time in the member initializer list using std::tie or something. However, you can put the initialization from foo into a static member function as follows:

#include <string_view>
#include <tuple>

std::tuple<int, int, int> foo(std::string_view sv);

class s {
    private:
    static s from_foo(std::string_view sv) {
        auto [x, y, z] = foo(sv);
        return {x, y, z};
    }

    s(int x, int y, int z) : x{x}, y{y}, z{z} {}

    public:
    s(std::string_view sv) : s(from_foo(sv)) {}

    private:
    int x, y, z;
};

If you're already providing a constructor that takes std::string_view, then it's not really leaking implementation details to do it this way. If you have three int members, then it's not really leaking implementation details to provide a constructor that initializes each. It would be leaking details to have a public constructor that takes std::tuple<int, int, int> though.

Also, it's debatable whether anything related to std::string_view -> s conversion should be inside the class at all. Parsing utilities are usually kept separate:

class s {
    public:
    s(int x, int y, int z) : x{x}, y{y}, z{z} {}

    private:
    int x, y, z;
};

std::tuple<int, int, int> foo(std::string_view sv);

s parse_s(std::string_view sv) {
    auto [x, y, z] = foo(sv);
    return {x, y, z};
}

If s really just stores three ints, it could also be an aggregate type; perhaps you don't need a constructor at all.

Sign up to request clarification or add additional context in comments.

4 Comments

It's not really "parsing", it behaves more like std::filesystem::directory_entry where the string is used as a key to fetch external data. I agree that adding s(int, int, int) makes most sense.
Do delegating constructors guarantee copy elision (prvalue semantics)? Otherwise, this would incur extra copy. More specifically, I can't tell whether the result object being initialized in "The result object of a prvalue is the object initialized by the prvalue" is the parameter of the target constructor or the original object being constructed.
@PasserBy the delegation s(from_foo(sv)) is delegating to the move constructor, so there is a temporary object involved so that the rvalue reference in the move constructor parameters can bind to something. I guess that's one more reason to have a separate parse_s function which returns s rather than relying on constructors.
Right, of course it's the move constructor, my mind is on copies for some reason :P
1

Actually you can initialize x, y and z at the same time in the member initializer, call foo only once, and avoid introducing any other constructors or member functions in your class s. But the solution may contain some implicit UB, see below.

if x, y, z were types without default constructor.

Let us first introduce such type instead of int:

// no default ctor and not copyable
struct A {
    int i;
    A(int ii) : i(ii) {}
    A(A&&) = default;
};

std::tuple<A, A, A> foo(std::string_view) {
    return { 1, 2, 3 };
}

struct S {
    S(std::string_view sv);
    A x, y, z;
};

The solution is based on the question Can constructor's member initializer include initialization of another member? (see the answer there about possible UB):

S::S(std::string_view sv)
    : x ( (
        [this](auto && t) {
            std::construct_at(&x, std::move(std::get<0>(t)));
            std::construct_at(&y, std::move(std::get<1>(t)));
            std::construct_at(&z, std::move(std::get<2>(t)));
        }(foo(sv)),
        [k=std::move(x)]()mutable { return std::move(k); }()
    ) )
    , y([k=std::move(y)]()mutable { return std::move(k); }())
    , z([k=std::move(z)]()mutable { return std::move(k); }())
{}

On practice, it works in GCC, Clang and MSVC: https://gcc.godbolt.org/z/484P79Mq1

We cannot omit initializers for all x, y, z. So y([k=std::move(y)]()mutable { return std::move(k); }()) just moves already initialized y back into y via a lambda's capture.

Comments

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.