“Static counter” for types behaves weirdly

2019-06-28 00:47发布

问题:

I'm developing an entity-based component system, and I'm trying to assign a certain index to component types:

static std::size_t getNextTypeId() {
    static std::size_t lastTypeIdBitIdx{0};
    ++lastTypeIdBitIdx;

    // This line produces the output at the end of the question
    std::cout << lastTypeIdBitIdx << std::endl; 

    return lastTypeIdBitIdx;
}

// I'm assuming that TypeIdStorage<T1>::bitIdx will always be different
// from TypeIdStorage<T2>::bitIdx
template<typename T> struct TypeIdStorage { 
    static const std::size_t bitIdx; 
};

// This line statically initializes bitIdx, getting the next id
template<typename T> const std::size_t TypeIdStorage<T>::bitIdx{getNextTypeId()};

In my game code I have about 20 component types declared like this:

struct CPhysics : public sses::Component { ... };
struct CHealth : public sses::Component { ... };
struct CWeapon : public sses::Component { ... };
// and so on...

In my entity system code I use TypeIdStorage<T>::bitIdx with T being one of the component types several times - I expect this to happen:

  • If TypeIdStorage<T> exists, simply return TypeIdStorage<T>::bitIdx.
  • If it doesn't exist, create it and initialize bitIdx with getNextTypeId().

This is what is printed when I run the application:

1 2 3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ...

How is it possible that calling getNextTypeId() returns the same number? This kind of output should be impossible.

Isn't it guaranteed that the static variable will get incremented without repeating? I'm truly confused here.


Tested both with g++ 4.8.1 and clang++ 3.4, both in debug and release modes. Same output.

valgrind does not print anything interesting.

clang++'s AddressSanitizer does not print anything interesting either.

Setting my program's entry point to int main() { return 0; } produces exactly the same output. The issue is at compile-time - but how is it possible? This seems like an impossible situation to me.

回答1:

You need to drop the static when declaring the function:

std::size_t getNextTypeId() {
    // ...
}

to make sure only one version of this function exists. For this, you probably also need to move the definition to an implementation file and only leave the declaration in the header.

If you declare the function static, it means that the symbol is not exported and can only be used in the same translation unit. It is no longer shared between the translation units. This leads to each translation unit having its own copy of the function and each copy has, of course, its own counter.



回答2:

You didn't post enough code to replicate the problem. However, if you have the above code in header file and use it from multiple translation units, you can get the observed behavior. The problem with the code in this case is that the same template code resolves to using different functions, i.e., different versions of getNextTypeId(). The fix to the problem is, of course, to not have getNextTypeId() be a static function but rather to use the same function in all cases, e.g., making it inline. For example:

  1. The header file (assumed to be in dcount.h):

    #include <iostream>
    
    static std::size_t getNextTypeId() {
        static std::size_t lastTypeIdBitIdx{0};
        ++lastTypeIdBitIdx;
    
        // This line produces the output at the end of the question
        std::cout << "last index=" << lastTypeIdBitIdx << '\n';
    
        return lastTypeIdBitIdx;
    }
    
    // I'm assuming that TypeIdStorage<T1>::bitIdx will always be different
    // from TypeIdStorage<T2>::bitIdx
    template<typename T> struct TypeIdStorage { 
        static const std::size_t bitIdx; 
    };
    
    // This line statically initializes bitIdx, getting the next id
    template<typename T> const std::size_t TypeIdStorage<T>::bitIdx{getNextTypeId()};
    
  2. first translation unit (assumed to be in dcount-t1.cpp):

    #include "dcount.h"
    
    struct A {};
    struct B {};
    struct C {};
    
    int f()
    {
        TypeIdStorage<A>::bitIdx;
        TypeIdStorage<B>::bitIdx;
        TypeIdStorage<C>::bitIdx;
    }
    
  3. second translation unit (assumed to be in dcount-t2.cpp):

    #include "dcount.h"
    
    struct D {};
    struct E {};
    struct F {};
    
    int g()
    {
        TypeIdStorage<D>::bitIdx;
        TypeIdStorage<E>::bitIdx;
        TypeIdStorage<F>::bitIdx;
    }
    
  4. Finally a program which pulls these together (dcount-main.cpp):

    extern void f();
    extern void g();
    
    int main()
    {
        f();
        g();
    }
    

Compiling these files using, e.g., g++ -std=c++11 -o dcount dcount-t1.cpp dcount-t2.cpp dcount-main.cpp yield an executable which replicates the behavior you noticed:

$ g++ -std=c++11 -o dcount dcount-t1.cpp dcount-t2.cpp dcount-main.cpp 
$ ./dcount 
last index=1
last index=2
last index=3
last index=1
last index=2
last index=3