2

When using g++ 3.4.6 (with ld 2.15.92.0.2), I can write:

class ConstantContainer {
public:
    static const uint16_t MyConstant1 = UINT16_C(0x4321);
    static const uint32_t MyConstant2 = UINT32_C(0x87654321);
    static const uint64_t MyConstant3 = UINT64_C(0xF0E1D2C3B4A59687);
    static const double MyConstant4 = 1.0 / 4.0;
};

and use ConstantContainer::MyConstant1 and others almost everywhere as a cheep substitute for scoped literals, with the exception of initializing other constants.

However, when using g++ 3.3.6 (with ld of the same version 2.15.92.0.2, although not the same binary and distro), the code compiles fine, too, but linking fails in some cases due to unresolved reference at any point a “constant” is used:

g++ -o myapp module1.o module2.o ... main.o
moduleN.o(.text+0x59e): In function `BlahBlah(const FooBar&)':
: undefined reference to `ConstantContainer::MyConstant1'

I could not figure out that are the unique features that provoke such a behavior. For example, a non-compatible case may be as simple as this:

class GraphConversionState {
public:
    struct NodeIndex {
    public:

        typedef CxxStd::uint32_t ValueType;
        ValueType Value;

        class ValueSpecial {
        public:
            static CxxConstExpr ValueType
                Unknown = UINT32_C(0xFF000000),
                Isolated = UINT32_C(0xFF111111),
                Connected = UINT32_C(0xFFCCCCCC);
        };
    };
};

I. e. there is only a small bunch of static constant members of uint type, yet they don't qualify to be treated as named literals; meanwhile, in other cases, even floating point values are fine. The only obvious difference is scope level (class nesting), but that doesn't prove to be the real reason in general case with simplified examples.

The obvious workaround is to turn the aforementioned class into a monster:

class ConstantContainerType {
public:
    uint16_t MyConstant1;
    uint32_t MyConstant2;
    uint64_t MyConstant3;
    double MyConstant4;
    ConstantContainerType() :
        MyConstant1(UINT16_C(0x4321)),
        MyConstant2(UINT32_C(0x87654321))
        MyConstant3(UINT64_C(0xF0E1D2C3B4A59687))
        MyConstant4(1.0 / 4.0)
        { }
};
static const ConstantContainerType ConstantContainer;

// in ConstantContainer.cpp:
const ConstantContainerType ConstantContainer;

But that is quite ugly, less clean and much more error-prone, as number of constants and container classes is high. Even more so, while in-place declared and defined constants are probably optimized as they were real literals, it is very doubtful they would be treated so when being a part of a singleton object.

So I wondered: what are the exact rules employed by GCC 3.3 and above for treating some static const POD declarations as constant definitions?

Anton Samsonov
  • 1,380
  • 17
  • 34
  • 3
    The obvious question: are you required to use gcc-3.x compilers? – Brett Hale Jan 29 '15 at 17:28
  • @BrettHale Unfortunately, I have to support such an old version — simply because it is the only one available on the target platform. I try to write future-proof code by using a bunch of `#define`s (a la Boost), but some fundamental concepts like this cannot be emulated easily. – Anton Samsonov Jan 29 '15 at 17:34
  • 2
    I believe `static const =` is fine in C++98, as long as it's an *integral* type. But you need the `const A::B;` in a translation unit, just in case someone uses: `& A::B` somewhere. You might just get lucky with gcc-3.4.x - i.e., it's performing constant folding; whereas gcc-3.3.x is trying to access the constant object. [ref](http://www.parashift.com/c++-faq/static-const-with-initializers.html), [more](http://stackoverflow.com/questions/177437/const-static) – Brett Hale Jan 29 '15 at 17:55
  • @BrettHale I updated my question to indicate that `static const =` is indeed supported in many cases, even with non-integer types. That puzzles me, however, is *when* that construct is *not* supported even for a native integer. – Anton Samsonov Feb 11 '15 at 16:14
  • 1
    There's always the `numeric_limits` style of using static member functions... – T.C. Feb 11 '15 at 17:05
  • @T.C. You mean defining each “constant” as a `return X;` function and hope the compiler will optimize it? That's a nice solution, but has one weak spot: you have to always remember to put parenthesis after a “constant” name (otherwise you would get a hard-to-diagnose function pointer instead of value) — which is intuitively difficult if you're accustomed to treat constants as variables rather than functions. – Anton Samsonov Feb 11 '15 at 17:45

3 Answers3

0

Either you can define it elsewhere,

class ConstantContainer {
public:
    static const uint16_t MyConstant1;
    static const uint32_t MyConstant2;
};
ConstantContainer::MyConstant1 = UINT16_C(0x4321);
ConstantContainer::MyConstant2 = UINT32_C(0x87654321);

or make the second cleaner by declaring the members as constants.

class ConstantContainer {
public:
    const uint16_t MyConstant1;
    const uint32_t MyConstant2;
    ConstantContainer(uint16_t foo, uint16_t bar) 
        :MyConstant1(foo), MyConstant2(bar)
    {}
};
user3528438
  • 2,737
  • 2
  • 23
  • 42
  • So, what's the difference? Your second example requires that a static instance `ConstantContainer` is created somewhere: one cannot simply access `ConstantContainer::MyConstant1` as a shared compile-time constant. – Anton Samsonov Feb 11 '15 at 16:27
  • @Anton Samsonov It depends on whether you want the same container class to contain same consts with different values in different instances. – user3528438 Feb 11 '15 at 16:49
  • In my understanding, “constants” are really global invariants, even if technically computed at run-time for code clarity and due to compiler shortcomings. That is, a “constant” is some value that will never change and is not dependent on current implementation, software version and other transient things. Otherwise it's not a “constant”, but a “run-time invariant”. – Anton Samsonov Feb 11 '15 at 17:04
  • @AntonSamsonov "Const" simply means "read-only" within a certain scope and time. You are always free to use it your way, although personally I'm very conservative when thinking about putting anything in global scope. – user3528438 Feb 11 '15 at 17:30
0

The old trick still works:

class ConstantContainer {
public:
    enum { MyConstant1 = UINT16_C(0x4321) };
    enum { MyConstant2 = UINT32_C(0x87654321) };
};

Of course, if you want actual objects of type uint16_t/uint32_t, they'll have to live somewhere.

MSalters
  • 173,980
  • 10
  • 155
  • 350
  • That trick looks like very error-prone, since the actual data type will be `int`, and as such it will expand beyond the expected `uint16` size, as well as truncate any `UINT64_C` value unless on a 64-bit platform. – Anton Samsonov Feb 11 '15 at 16:18
  • 1
    @AntonSamsonov: Nope, the actual type will be an (unnamed) enum type of unspecified but sufficient width. You might be confused with C, where the type would be `int` - except when it would be bigger. C like C++ has the "sufficient width" rule. And since C99 states that there must be a 64 bit `unsigned long long` (even on 8 bit platforms!) a C99 compiler can't truncate `UINT64_C`. C++98 technically didn't mandate 64 bit enums, but every relevant player had 64 bit types anyway. – MSalters Feb 11 '15 at 18:41
0

Just use a namespace instead of abusing classes like that:

namespace ConstantContainer {
    uint16_t const MyConstant1 = UINT16_C(0x4321);
    uint32_t const MyConstant2 = UINT32_C(0x87654321);
}

In addition, constants at namespace scope have internal linkage by default (like static objects at namespace level) so you can declare and define them in a header file without risking ODR violations.

Ulrich Eckhardt
  • 16,572
  • 3
  • 28
  • 55
  • Classes are just scope-narrowing containers available inside other classes. I don't know about related compiler optimizations you mention, but, from the code readability perspective, moving constants up to the namespace level is not any better than using `#define`-constants in the global scope. – Anton Samsonov Feb 11 '15 at 16:22
  • Hello @AntonSamsonov! I don't mention any compiler optimizations. Also, I don't see how constants in a namespace are in any way similar to a macro (how can a macro be in the global scope, btw?). Could it be that you are commenting the wrong answer? – Ulrich Eckhardt Feb 11 '15 at 19:10