By Frank


2019-06-07 16:05:28 8 Comments

The following block of code:

  1. Is technically invalid, since std::get<>() is not thread safe. Reference: Is using `std::get<I>` on a `std::tuple` guaranteed to be thread-safe for different values of `I`?

  2. As far as I can tell, is effectively safe on all implentations of std::tuple<> in the wild right now, and the foreseeable future.

    #include <tuple>
    #include <atomic>
    #include <thread>

    // Out of my control
    using data_t = std::tuple<int, int, int, int>;
    void foo(data_t); 
    //

    int main() {
        data_t landing;
        std::atomic<int> completed = 0;

        // Whichever thread pings last will be the one performing foo()
        auto ping = [&](){
            if(++completed == 4) {
                foo(landing);
            }
        };

        std::thread a([&](){ std::get<0>(landing) = 1; ping(); });
        std::thread b([&](){ std::get<1>(landing) = 2; ping(); });
        std::thread c([&](){ std::get<2>(landing) = 3; ping(); });
        std::thread d([&](){ std::get<3>(landing) = 4; ping(); });

        a.join();
        b.join();
        c.join();
        d.join();

        return 0;
    }

To make matters even more fun, the actual code in question is chock-full of variadic templates, so writing a one-shot landing pad struct to handle that one scenario is not going to cut it. It has to be a general solution.

My current options are:

  • Effectively re-implement std::tuple<> with a reworded std::get<> documentation, which is a waste of time and a waste of code.
  • Push a proposal for std::get<>(std::tuple) to provide guarantees similar to std::vector<>, and document the fact that the code is only valid as of a yet unreleased verion of the standard.
  • ignore the issue, and rely on the fact that realistically, this will almost certainly always work.

None of those are particularly great in the short run... So my questions are:

  • Did I miss something that invalidates point #2?
  • Is there a better workaround that would allow the implementation to be technically valid while not having to support an excessive amount of extra code.
  • Any other opinions on the subject are welcome.

2 comments

@Yakk - Adam Nevraumont 2019-06-07 17:12:26

template<class...Ts>
std::tuple< std::remove_reference_t<T>* >
to_pointers( std::tuple<Ts...>& );

template<class T0, class...Ts>
std::array<T0, 1+sizeof...(Ts)>
to_array( std::tuple<T0, Ts...> const& );

write these.

int main() {
    data_t landing;
    std::atomic<int> completed = 0;

    // Whichever thread pings last will be the one performing foo()
    auto ping = [&](){
        if(++completed == 4) {
            foo(landing);
        }
    };

    auto arr = to_array(to_pointers(landing));

    std::thread a([&](){ *arr[0] = 1; ping(); });
    std::thread b([&](){ *arr[1] = 2; ping(); });
    std::thread c([&](){ *arr[2] = 3; ping(); });
    std::thread d([&](){ *arr[3] = 4; ping(); });

    a.join();
    b.join();
    c.join();
    d.join();

    return 0;
}

we access the tuple elements via pointers to them instead of via std::get. The problem is in the specification of std::get; once you have the independent pointers to the independent objects guaranteed to exist within the tuple, you are race condition free.

So we convert the tuple to an array of pointers in one thread (which is basically free), then use it in the threads safely.

@SergeyA 2019-06-07 17:29:38

But now you have introduced indirection, and you are at the mercy of the compiler to optimize it away.

@Frank 2019-06-07 17:32:23

It's also a valid solution for this MCVE, but if the async operation's scope lives beyond the original calling function, those pointers will have to be heap-allocated alonside the landing.

@Yakk - Adam Nevraumont 2019-06-07 17:34:06

@Frank Sure, or you could have the threads capture their pointers by value. [ptr = arr[0]]{ *ptr = 1; ping(); } . The point of this is twofold; (1) a tuple of identical types is a std::array really, and (2) storing references or pointers into the tuple is legal even if std::get<I> isn't race-free.

@Vittorio Romeo 2019-06-07 16:27:47

Push a proposal for std::get<>(std::tuple) to provide guarantees similar to std::vector<>, and document the fact that the code is only valid as of a yet unreleased version of the the standard.

I think this is the way to go, as it provides value for the entire C++ community and should not be a burden on implementers. It is also an excellent opportunity to write your first proposal.

I suggest doing that, and for now assuming that this will work, even though it is UB. If your software is super-critical (e.g. flight control system) and you want to be 100% sure that you are not relying on something that could break in the future... then implement your own tuple.

@Nicol Bolas 2019-06-07 16:31:07

Note that this should be extended to other uses of get defined by the standard library. array, and pair should have the same protections. Indeed, it might be good to say that, if a type is decomposable (usable in structured bindings), then the user code invoked must not provoke a data race when accessing distinct indices.

@Frank 2019-06-07 17:25:14

@NicolBolas I'm not so sure about that last point. I could see scenarios where you would want different indices to alias the same data. A lazy-evaluated vector swizzle operation for example.

@SergeyA 2019-06-07 17:34:30

@Frank that doesn't really matter. The get operation itself should be thread safe, as well as accessing the elements of the array created from tuple. The elements themselves might not be necessarily thread-safe - like pointers could point to the same object, and thus writing through the pointer is not thread safe.

@Frank 2019-06-07 17:38:42

@SergeyA I'm saying that I can conceive of a type T where float& std::get<0>(T&) and float& std::get<1>(T&) return two references to the same float object. If you think that this should be fine, then the "when accessing distinct indices" part of NicolBolas' suggestion might as well be dropped.

@Nicol Bolas 2019-06-07 17:42:59

@Frank: "A lazy-evaluated vector swizzle operation for example." GLSL's swizzle mask explicitly forbids the use of an lvalue swizzle that accesses the same component twice as an lvalue (vec.xyyz = ... is a compile error). So a similar swizzle operation would return a T, not a T&. That would make such things read accesses through a const& parameter, and therefore not provoke a data race.

Related Questions

Sponsored Content

25 Answered Questions

[SOLVED] How to convert std::string to lower case?

22 Answered Questions

[SOLVED] How to concatenate a std::string and an int?

15 Answered Questions

[SOLVED] How can I convert a std::string to int?

  • 2011-10-05 15:23:45
  • Brandon
  • 1208499 View
  • 417 Score
  • 15 Answer
  • Tags:   c++ string int

18 Answered Questions

[SOLVED] How to find out if an item is present in a std::vector?

  • 2009-02-20 21:58:41
  • Joan Venge
  • 855392 View
  • 575 Score
  • 18 Answer
  • Tags:   c++ vector std

8 Answered Questions

[SOLVED] How to convert a std::string to const char* or char*?

  • 2008-12-07 19:30:56
  • user37875
  • 876023 View
  • 854 Score
  • 8 Answer
  • Tags:   c++ string char const

5 Answered Questions

[SOLVED] How exactly is std::string_view faster than const std::string&?

7 Answered Questions

2 Answered Questions

[SOLVED] How does std::tie work?

  • 2017-05-03 14:17:56
  • bolov
  • 22069 View
  • 100 Score
  • 2 Answer
  • Tags:   c++ c++11 tuples

1 Answered Questions

Sponsored Content