C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Suggestion: non-static member variables for static-duration-only classes

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Fri, 3 Oct 2025 14:12:52 -0400
On Thu, Oct 2, 2025 at 4:59 PM Walt Karas via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> I suggest allowing non-static thread_local member variables for classes,
> which would implicitly restrict instances of the class to have static
> storage duration.
>

At first glance, I thought this made some sense. Consider that we are
currently allowed to write collections of related objects like this:
    int g_i;
    thread_local int g_j;
    void f() {
        static int s_i;
        static thread_local int s_j;
    }
And yet C++ doesn't currently allow us to package these related objects up
into a struct:
    struct Package { int i; thread_local int j; };
    Package g;
    void f() {
        static Package s;
    }

However, after more thought and experimentation, I think the
counter-argument is that we are *also* allowed to write:
    void g() {
        int p_m;
        *static* int p_n;
    }
and yet that's *not* equivalent to
    struct Package2 { int m; *static* int n; };
    void g() {
        Package2 p;
    }
(Specifically, the initialization around `n` differs.) So why should it
work any better if we try to replace the `static` keyword with the
`thread_local` keyword? Or `inline`, or `register`, or...

Thread_locals have weird initialization rules — even weirder than
statics/globals. If you were allowed to write
    struct Package { int i; thread_local int j; }
then you might expect that the constructor of `Package p` should be
responsible for constructing both `p.i` and `p.j`. But that can't be true,
because the constructor of `p` is called only once, and `p.j` needs to be
initialized as many times as you have threads. So that can't possibly work.

Consider also that thread_local variables already can have their
initializers skipped over:
// https://godbolt.org/z/6cbj63YzP
int main() {
thread_local std::string s = "hello world";
std::thread t([]() {
// when used here, s is uninitialized
printf("%s\n", s.c_str());
});
t.join();
}

And they invariably have their destructors delayed to the end of the
thread, instead of being destroyed in reverse order of construction (Godbolt
<https://godbolt.org/z/oGrxbE68f>). So they don't behave in an RAII
fashion. It would be *nice* if they behaved better, but I'm not sure that's
physically possible. Allowing them to "live inside" classes and RAII types
might be a moral hazard, by implying that they work better than in fact
they do.

If I had my druthers, C++ would never have standardized a `thread_local`
keyword to begin with. I doubt it's possible to use that keyword safely or
portably, even 14 years after it was invented.

This would have been nice to have for a class I'm currently working on. The
> only work-around I could to come up with was [...]


FYI, you never explained what this class was or how thread_locals would
have been useful to it. I bet there was a better way to do what you were
trying to do; and I bet the `thread_local` keyword was the wrong tool,
anyway.

–Arthur

Received on 2025-10-03 18:13:09