c++constructortuplesinitializer-list

std::tie in constructor's initializer list


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?


Solution

  • 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.