Some helper functions for interlocked pointer operations


The pointer-related Interlocked functions all operate on void*. In practice, though, you are operating on typed pointers. Here are some helper functions to save you a bunch of typing.

template<typename T, typename U>
T* InterlockedExchangePointerT(
    T* volatile *target,
    U value)
{
  return reinterpret_cast<T*>(InterlockedExchangePointer(
    reinterpret_cast<void* volatile*>(target),
    static_cast<T*>(value)));
}

// Repeat for InterlockedExchangePointerAcquire and
// InterlockedExchangePointerNoFence.

template<typename T, typename U, typename V>
T* InterlockedCompareExchangePointerT(
    T* volatile *target,
    U exchange,
    V compare)
{
  return reinterpret_cast<T*>(InterlockedCompareExchangePointer(
    reinterpret_cast<void* volatile*>(target),
    static_cast<T*>(exchange),
    static_cast<T*>(compare)));
}

// Repeat for InterlockedCompareExchangePointerAcquire,
// InterlockedCompareExchangePointerRelease, and
// InterlockedCompareExchangePointerNoFence.

The naïve versions of these functions would be

template<typename T, typename U>
T* InterlockedExchangePointerT(
    T* volatile *target,
    T* value)
{
  return reinterpret_cast<T*>(InterlockedExchangePointer(
    reinterpret_cast<void* volatile*>(target),
    value));
}

template<typename T, typename U, typename V>
T* InterlockedCompareExchangePointerT(
    T* volatile *target,
    T* exchange,
    T* compare)
{
  return reinterpret_cast<T*>(InterlockedCompareExchangePointer(
    reinterpret_cast<void* volatile*>(target),
    exchange,
    compare));
}

but those simpler versions fail on things like

class Base { ... };
class Derived : public Base { ... };

extern Base* b;

Derived* d = new Derived();
if (InterlockedCompareExchange(&p, d, nullptr)) ...

because the compiler wouldn't be able to choose a value for T. From the first paramter, it would infer that T = Base; from the second parameter, it would infer that T = Derived; and from the third parameter, it would give up because it can't figure out what value of T would result in T* being the same as std::nullptr_t.

(You can guess how I discovered these limitations of the naïve versions.)

Comments (25)
  1. acq says:

    > if (InterlockedCompareExchange(

    Maybe "if (InterlockedCompareExchangeT("  ?

  2. Smithers says:

    > > if (InterlockedCompareExchange(

    > Maybe "if (InterlockedCompareExchangeT("  ?

    Maybe "if (InterlockedCompareExchangePointerT("?

    If you're going to be pedantic, you may as well be accurate. You could be thorough too: the first argument &p should presumably be &b.

    More telling than simple substitutions, however, is the way the naïve "InterlockedCompareExchangePointerT" fails when you *do* use three pointers of the same type: the compiler will complain that it can't deduce the template parameters U and V. From this, I deduce that Raymond had previously deleted the naïve functions and has attempted to reconstruct them from the fixed versions.

  3. ranta says:

    The static_cast in the recommended InterlockedExchangePointerT hides this kind of bug:

    class Base { };

    class Derived : public Base { };

      Derived* p = nullptr;

      InterlockedExchangePointerT(&p, new Base);

    Now, p points to a Base and the compiler did not warn.

    I suggest replacing the static_cast with an assignment. That will still allow a conversion from Derived* to Base* but not vice versa.

      T* tvalue = value;

      return reinterpret_cast<T*>(InterlockedExchangePointer(

         reinterpret_cast<void* volatile*>(target),

         tvalue));

    I don't know how often that kind of bug would occur in a real program, though.

  4. kantos says:

    Or I could just use std::atomic<T*> which does all of this for me… and VS2015 supports fully

  5. pm100 says:

    I would be very interesting to understand why you need volatile in the 'target' parameter. volatile is one of the keywords that usually gets inserted when not needed (or even when it has a bad effect) and not inserted when it is needed – I trust Raymond to have it right and would like to know what the logic is

  6. Darran Rowe says:

    @kantos:

    And those stuck using earlier versions where there is no std::atomic should do what exactly?

    I love preaching that people should use the latest and greatest tools myself, but you do come across companies and stuff that don't use VS2015, or VS2013, or… you get the idea.

    @ranta:

    To be honest, I would be more tempted to fix that using static_assert or a failed template instantiation at compile time if you can't use static_assert. This at least makes it look like it should fail if it is wrong. If you use an assignment, someone may "helpfully" come along at some later date and remove the assignment because it is a waste.

  7. Jo says:

    You really should consider replacing those reinterpret_casts with static_casts for increased type safety. You can always cast any pointer to void* and back using static_cast, but it will prevent you from e.g. casting to an int or something like that.

  8. Darran Rowe says:

    @pm100:

    Well, the parameters actually match the underlying API functions. These then match the underlying compiler intrinsic functions that are used to implement them when needed. So basically, he was just matching the existing functions.

    As for why this is needed, I would assume multi CPU systems would still be the biggest reason. Since while multi core systems have cache coherency stuff, it is normally to expensive to do it over multiple CPUs. Also, Windows has supported other CPU architectures, Windows currently supports ARM in addition to x86 based, and may support others in the future. So can you guarantee that in this case volatile will never have an effect?

  9. Darran Rowe says:

    @Jo:

    He can't. While it is acceptable to cast between void * and type * using static cast. The standard doesn't allow casting between void ** and type **.

    If you convert one of them to static casts, the compiler will complain:

    1>  main.cpp

    1>c:users***documentsvisual studio 2015projectsmehmehmain.cpp(11): error C2440: 'static_cast': cannot convert from 'derived *volatile *' to 'void *volatile *'

    1>  c:users***documentsvisual studio 2015projectsmehmehmain.cpp(11): note: Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or function-style cast

    1>  c:users***documentsvisual studio 2015projectsmehmehmain.cpp(36): note: see reference to function template instantiation 'T *InterlockedExchangePointerT<derived,base*>(T *volatile *,U)' being compiled

    1>          with

    1>          [

    1>              T=derived,

    1>              U=base *

    1>          ]

  10. Darran Rowe says:

    *sigh* lots of posts.

    I forgot, if you meant the reinterpret_cast on the return statements, then that is locked to the first parameter. So the static cast, bar the one problem pointed out, on the value parameter should pretty much handle any problematic type conversions.

    So while it would look nicer, that reinterpret_cast shouldn't have any problems.

    But I could be wrong.

  11. Medinoc says:

    I've been using some helper functions like these, but ran afoul of some of their limitations, such as their lack of support for const pointers (which cause the reinterpret_cast (possibly replaceable by static_cast) to fail compilation).

  12. kantos says:

    @DarranRowe ideally? they are using a smart pointer library, or write their own atomic smart pointer, preferably one that is easy to switch over to std::atomic later

  13. anon says:

    You can remove typename U, typename V from the naive versions (sorry for not including the dots above the i).

  14. Darran Rowe says:

    @kantos:

    So essentially, they would write their own which would end up calling similar helper functions right?

    The thing is, I wouldn't actually call these functions directly in my code either. Unless you are writing code similar to C with classes (and templates in this instance), then it is just natural that you would wrap these things. But they are still an important implementation detail in atomic code even if you don't use them directly.

  15. Ben Voigt says:

    @pm100: I believe the idea is that the objects being operated on by these functions should be defined `volatile`, thus preventing accidentally passing them to functions that don't use interlocked accesses.  (Which isn't ideal, since it prevents memset-ing the block during initialization when it isn't yet being shared and therefore ordinary access is ok, but still having the type checker identify problems is worth the added complexity at initialization, IMO)

  16. ranta says:

    @Darran Rowe, now that you suggested it, I tried the following:

    – typename std::enable_if<std::is_convertible<U, T*>::value, T*>::type

    – static_assert(std::is_convertible<U, T*>::value, "cannot convert from U to T*");

    and I found that the error message from the assignment was easiest to understand, static_assert came second, and enable_if was the hardest.

  17. Myria says:

    I pretty much ran into the same issues as you, Raymond, when implementing the same custom wrappers for the raw operations.  I had more or less the same solution.

    One annoyance is that Visual C++ does not implement _InterlockedCompareExchangePointer for x86-32; on x86-32 you have to use _InterlockedCompareExchange and some lovely reinterpret_casts.

  18. alegr1 says:

    Been there, wrote that.

  19. Darran Rowe says:

    @ranta:

    Yeah, those errors can be a pain. That is why I am waiting for concepts to get implemented properly.

    But right now, while it is true that the error for the assignment may be easier to understand, as I mentioned originally, the reason why I would prefer something like static_assert or something similar is that it looks like it is meant for error checking. Even if you put a comment by the assignment, it doesn't stop someone who knows better coming along in the future and getting rid of it. This is especially true if you don't own the code.

    Sometimes you have to take the rough with the smooth and take a more complex error message.

  20. Jim says:

    I'm not sure whether the naive versions are correct or not, but I am sure that the example code at the very end *ought* to lead to a compilation error (even ignoring the typos). Just from the types, it's not clear that b really has a pointer to Dervied, and yet it could end up in d.

    If you want to signal to the compiler, "no really, I'm sure that b is actually a pointer to Derived (at least if a swap is going to take place)" then the way to do that is with a cast. (A static cast, as others have said.) What's more, the cast also signals that information to the developer reading the code, so it's important that it stay at the point of the call, not in the wrapper function.

    The original point of having templates as a language feature of C++ at all is to catch errors at compile time using the type system even in generic situations. In fact, that's what it has successfully done for you here in the naive code.

  21. Neil says:

    Surely the solution is to make it so that the compiler can't deduce the type of the subsequent arguments? Something like this:

    template<typename T>

    T* InterlockedCompareExchangePointerT(

       T* volatile *target,

       typename std::identity<T>::type* exchange,

       typename std::identity<T>::type* compare)

  22. Alex Cohn says:

    @ranta:

    static_assert() looks OK for me in g++.

  23. ranta says:

    Neil, thank you. std::identity is excellent here.  Like with std::enable_if, the error message from an incorrect call refers primarily to the call, and Intellisense notices the error too.  Unlike with std::enable_if, the error message clearly shows the types.  And in code like the following,

         BSTR p = nullptr;

         CComBSTR cbs(OLESTR("leap"));

         InterlockedExchangePointerT(&p, cbs);

    the conversion to BSTR is done from cbs itself and not from a temporary copy, so the pointer stored in p will remain valid until cbs is destroyed.  (This kind of time-bomb pointer seems pretty dangerous for multithreaded operation though.)

  24. TC says:

    @Neil:

    std::identity is not actually standard, and MSVC's version actually breaks when used with nonreferenceable types (in particular, void). Of course, writing one yourself is trivial.

    Alternatively, typename std::add_pointer<T>::type (instead of typename std::identity<T>::type*) also works.

  25. DWalker says:

    Is InterlockedIncrement implemented in silicon yet?  

    It seems that a smart CPU and a smart memory bus ought to be able to increment data that resides in memory (at a given address and size) with a single, atomic instruction, with access barriers and such.

Comments are closed.

Skip to main content