Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compile-time counter in template class

I have a compile-time counter that I used for years, inspired by these answers. It works in C++03/11, and as far as I tested, relatively well on major compilers:

namespace meta
{
    template<unsigned int n> struct Count { char data[n]; };
    template<int n> struct ICount : public ICount<n-1> {};
    template<> struct ICount<0> {};

    #define MAX_COUNT 64
    #define MAKE_COUNTER( _tag_ ) \
        static ::meta::Count<1> _counter ## _tag_ (::meta::ICount<1>)
    #define GET_COUNT( _tag_ ) \
        (sizeof(_counter ## _tag_ (::meta::ICount<MAX_COUNT + 1>())) - 1)
    #define INC_COUNT( _tag_ ) \
        static ::meta::Count<GET_COUNT(_tag_) + 2> _counter ## _tag_ (::meta::ICount<2 + GET_COUNT(_tag_)>)
}

The following test compiles and runs perfectly (expected output is 0 1 2 3):

struct Test
{
    MAKE_COUNTER( uu );

    static const unsigned int a = GET_COUNT( uu );
    INC_COUNT( uu );
    static const unsigned int b = GET_COUNT( uu );
    INC_COUNT( uu );
    static const unsigned int c = GET_COUNT( uu );
    INC_COUNT( uu );
    static const unsigned int d = GET_COUNT( uu );

};

template<typename T>
void test()
{
    std::cout << T::a << " " << T::b << " " << T::c << " " << T::d << "\n";
}

int main()
{
    test<Test>();
}

However, I found a case were I see a very strange behavior happening with clang and gcc. If you change Test to be a template struct, taking an int for example (template<int> struct Test, and test<Test<42> >() in main), clang and gcc both fail to compile, complaining that I am redefining the counter function (while msvc compiles it without problems). For some reason the compiler fails to compute a sizeof in a template class.

clang find the error at the third INC_COUNT, while gcc find it at the second one.

I manually expanded this macro, and:

  • for clang, it gives

    static ::meta::Count<GET_COUNT(uu)+2> _counteruu(::meta::ICount<(sizeof(_counteruu(::meta::ICount<65>())) - 1)+2>);
    //                                                              ^                                            ^
    

    removing the underlined parentheses solves the issue.

  • for gcc: moving the +2 before the sizeof is the only work-around

The sad note is that these workarounds seem not to work when included in the macros. It's like the compiler just forgets how to compute the result of sizeof after some time...

Why is this happening ? Am I doing something wrong, or is it just compiler bugs (since clang and gcc don't even report the same line) ?

Note: I know there is a gcc bug about this counter. The question is not about this bug.

like image 437
Synxis Avatar asked Jul 30 '15 09:07

Synxis


People also ask

Are templates compile-time?

The use of templates can be thought of as compile-time polymorphism. The technique is used by a number of languages, the best-known being C++, but also Curl, D, Nim, and XL.

Are templates evaluated at compile-time?

1.1.2 Using Function Templates A template is visited twice by the compiler. On first pass it's simply checked for correct syntax. It's only actually compiled when it is used (instantiated) in code.

Are C++ templates compile-time?

3 Compile-Time Instantiation. Instantiation is the process by which a C++ compiler creates a usable function or object from a template. The C++ compiler uses compile-time instantiation, which forces instantiations to occur when the reference to the template is being compiled.


1 Answers

Your code is ill-formed, no diagnostic required. §3.3.7/1, second bullet point1:

A name N used in a class S shall refer to the same declaration in its context and when re-evaluated in the completed scope of S. No diagnostic is required for a violation of this rule.

You use overload resolution to select the appropriate overload of _counteruu. However, in the initializer of e.g. a, an overload (=declaration) is selected that wouldn't be selected if we were to perform overload resolution at the end of Test, such as in the initializer of d. Hence _counteruu refers to another, distinct declaration when re-evaluated in the completed scope of Test.

To show up which calls exactly I'm referring to, consider the preprocessed definition of Test:

struct Test
{
    // (1)
    static ::meta::Count<1> _counteruu (::meta::ICount<1>);
    static const unsigned int a = (sizeof(_counteruu (::meta::ICount<64 + 1>())) - 1);
    // (2)
    static ::meta::Count<(sizeof(_counteruu (::meta::ICount<64 + 1>())) - 1) + 2> _counteruu (::meta::ICount<(sizeof(_counteruu (::meta::ICount<64 + 1>())) - 1) + 2>);
    static const unsigned int b = (sizeof(_counteruu (::meta::ICount<64 + 1>())) - 1);
    // (3)
    static ::meta::Count<(sizeof(_counteruu (::meta::ICount<64 + 1>())) - 1) + 2> _counteruu (::meta::ICount<(sizeof(_counteruu (::meta::ICount<64 + 1>())) - 1) + 2>);
    static const unsigned int c = (sizeof(_counteruu (::meta::ICount<64 + 1>())) - 1);
    // (4)
    static ::meta::Count<(sizeof(_counteruu (::meta::ICount<64 + 1>())) - 1) + 2> _counteruu (::meta::ICount<(sizeof(_counteruu (::meta::ICount<64 + 1>())) - 1) + 2>);
    static const unsigned int d = (sizeof(_counteruu (::meta::ICount<64 + 1>())) - 1);

};

Simplification yields

struct Test
{
    // (1)
    static ::meta::Count<1> _counteruu (::meta::ICount<1>);
    static const unsigned int a = (sizeof(_counteruu (::meta::ICount<65>())) - 1);
    // (2)
    static ::meta::Count<2> _counteruu (::meta::ICount<2>);
    static const unsigned int b = (sizeof(_counteruu (::meta::ICount<65>())) - 1);
    // (3)
    static ::meta::Count<3> _counteruu (::meta::ICount<3>);
    static const unsigned int c = (sizeof(_counteruu (::meta::ICount<65>())) - 1);
    // (4)
    static ::meta::Count<4> _counteruu (::meta::ICount<4>);
    static const unsigned int d = (sizeof(_counteruu (::meta::ICount<65>())) - 1);
};

We can clearly see how the mechanism works now: Overload resolution will prefer the last added overload when ICount<some sufficiently large number> is passed due to the way derived-to-base conversions are ranked. However, the call in the initializer of a will select the first overload; But re-evaluating this initializer would select the last one.


1 This bullet point existed in C++03 as well, but in §3.3.6.

like image 178
Columbo Avatar answered Sep 21 '22 23:09

Columbo