Check if a function argument value is thread-local

In C++, is it possible to check (preferably at compile time) if a function argument reference value is thread-local?

E.g.

void foo( int& bar ) { ... }

I’d like to check/enforce that bar refer to a thread_local.

Assume C++17 or later on Windows and/or Fedora.

  • 1

    I would be surprised if there is a way. You can use things like <type_traits> to perform compile time checks against types but I’m not aware of any analog for storage class specifiers.

    – 

  • 1

    as far as I know there is know way to know the storage duration of the variable. You might be able to use its address but that would be very implementation dependent.

    – 

  • 1

    @Artyer A thread-local variable has thread storage duration, not static storage duration, so a lvalue to it doesn’t qualify as constant expression. It shouldn’t be permitted as template argument.

    – 

  • @user17732522 Checking the storage duration is an interesting idea. Theoretically, it should work. I’m looking at the mechanics/syntax.

    – 




  • 1

    @Underhill No, that was a reply to a now removed comment. The point is that you can’t use the approach from the original comment to detect static storage duration, because thread-local variables do not have static storage duration. I think it is technically possible to determine whether the name of a variable refers to a thread-local variable with the help of macros at compile-time by making use of some special cases in constant expression evaluation, but I think that wouldn’t be very stable and likely wouldn’t work in all compilers as it should.

    – 




No you cannot check at compile time. However you can do something to at least document intent and do some compile time checking like this (by letting the type system do some work for you):

#include <iostream>

// Declare/define a class that can behave stand in for type_t
// but also describes intent
// more constructors/destructors might need to be added... this is just a sketch
template<typename type_t>
class thread_local_t
{
public:
    explicit thread_local_t(const type_t& value) :
        m_value{value}
    {
    }

    operator type_t&() noexcept
    {
        return m_value;
    }

    operator const type_t&() const noexcept
    {
        return m_value;
    }

private:
    type_t m_value;
};

// accept only a thread_local_t<int> (decorated) int
// since thread_local_t is not implicitly convertible from int
// this adds an extra compile time barrier
int foo(const thread_local_t<int>& value)
{
    std::cout << value;
    return value;
}

int main()
{
    thread_local thread_local_t<int> value{42}; // <== create a thread_local "int" here
    auto retval = foo(value);
    // foo(42);  <== will not compile
    return retval;
}

template<class T, auto Key>
struct thread_local_decl_t;

template<class T>
struct thread_local_t {
  template<class, auto>
  friend struct thread_local_decl_t;
  operator T&() noexcept {
    return get();
  }
  operator T const&() const noexcept {
    return get_const();
  }
private:
  T&(*gettor)() = nullptr;
  thread_local_t(T&(*gettor_arg)()):
    gettor(gettor_arg)
  {}
  T& get() { return gettor(); }
  T const& get_const() const { return gettor(); }
};
template<class T, auto Key>
struct thread_local_decl_t:
  thread_local_t<T>
{
public:
  static T& get_ctor(std::function<T()> ctor) {
    thread_local T t{ctor()};
    return t;
  }
  static T& get() {
    return get_ctor(nullptr);
  }
  static T const& get_const() {
    return get(nullptr);
  }
  template<class...Args>
  explicit thread_local_decl_t(Args&&...args):
    thread_local_t<T>(&thread_local_decl_t::get) 
  {
    // construct the thread-local
    get_ctor( [&]()->T{ return T(std::forward<Args>(args)...); } );
  }
  thread_local_decl_t(thread_local_decl_t const&)=default;
  thread_local_decl_t(thread_local_decl_t &&)=default;

};

#define THREAD_LOCAL(...) \
  thread_local_decl_t< __VA_ARGS__, []{} >


void print( thread_local_t<int> x ) {
  std::cout << x << "\n";
}

void test() {
  auto tl_int = THREAD_LOCAL(int)( 7 );
  print(tl_int);
}
int main() {
  test();
}

this bit of evil uses a lambda to create a thread local object within the thread_local_decl_t that is unique to each use site. We can pass thread local values around and verify they are indeed thread local with the C++ type system by doing a thread_local_t<int>.

thread_local_t<X> is a reference to a thread local value, while auto name = THREAD_LOCAL(X)(construction arguments) declares a thread local value, using this system.

Requires c++20.

Leave a Comment