This is an unofficial snapshot of the ISO/IEC JTC1 SC22 WG21 Core Issues List revision 115f. See http://www.open-std.org/jtc1/sc22/wg21/ for the official list.

2024-12-06


454. When is a definition of a static data member required?

Section: 11.4.9.3  [class.static.data]     Status: CD1     Submitter: Gennaro Prota     Date: 18 Jan 2004

[Voted into WP at the October, 2006 meeting.]

As a result of the resolution of core issue 48, the current C++ standard is not in sync with existing practice and with user expectations as far as definitions of static data members having const integral or const enumeration type are concerned. Basically what current implementations do is to require a definition only if the address of the constant is taken. Example:

void f() {

  std::string s;
  ...

  // current implementations don't require a definition
  if (s.find('a', 3) == std::string::npos) {
   ...
  }

To the letter of the standard, though, the above requires a definition of npos, since the expression std::string::npos is potentially evaluated. I think this problem would be easily solved with simple changes to 11.4.9.3 [class.static.data] paragraph 4, 11.4.9.3 [class.static.data] paragraph 5 and 6.3 [basic.def.odr] paragraph 3.

Suggested resolution:

Replace 11.4.9.3 [class.static.data] paragraph 4 with:

If a static data member is of const integral or const enumeration type, its declaration in the class definition can specify a constant-initializer which shall be [note1] an integral constant expression (5.19). In that case, the member can appear in integral constant expressions. No definition of the member is required, unless an lvalue expression that designates it is potentially evaluated and either used as operand to the built-in unary & operator [note 2] or directly bound to a reference.

If a definition exists, it shall be at namespace scope and shall not contain an initializer.

In 11.4.9.3 [class.static.data] paragraph 5 change

There shall be exactly one definition of a static data member that is used in a program; no diagnostic is required; see 3.2.

to

Except as allowed by 9.4.2 par. 4, there shall be exactly one definition of a static data member that is potentially evaluated (3.2) in a program; no diagnostic is required.

In 6.3 [basic.def.odr] paragraph 3 add, at the beginning:

Except for the omission allowed by 9.4.2, par. 4, ...

[note 1] Actually it shall be a "= followed by a constant-expression". This could probably be an editorial fix, rather than a separate DR.

[note 2] Note that this is the case when reinterpret_cast-ing to a reference, like in

struct X { static const int value = 0; };
const char & c = reinterpret_cast<const char&>(X::value);
See 7.6.1.10 [expr.reinterpret.cast]/10

More information, in response to a question about why issue 48 does not resolve the problem:

The problem is that the issue was settled in a way that solves much less than it was supposed to solve; that's why I decided to file, so to speak, a DR on a DR.

I understand this may seem a little 'audacious' on my part, but please keep reading. Quoting from the text of DR 48 (emphasis mine):

Originally, all static data members still had to be defined outside the class whether they were used or not.

But that restriction was supposed to be lifted [...]

In particular, if an integral/enum const static data member is initialized within the class, and its address is never taken, we agreed that no namespace-scope definition was required.

The corresponding resolution doesn't reflect this intent, with the definition being still required in most situations anyway: it's enough that the constant appears outside a place where constants are required (ignoring the obvious cases of sizeof and typeid) and you have to provide a definition. For instance:

  struct X {
   static const int c = 1;
  };

  void f(int n)
  {
   if (n == X::c)   // <-- potentially evaluated
    ...
  }

<start digression>

Most usages of non-enum BOOST_STATIC_COSTANTs, for instance, are (or were, last time I checked) non-conforming. If you recall, Paul Mensonides pointed out that the following template

// map_integral

template<class T, T V> struct map_integral : identity<T> {
  static const T value = V;
};

template<class T, T V> const T map_integral<T, V>::value;

whose main goal is to map the same couples (type, value) to the same storage, also solves the definition problem. In this usage it is an excellent hack (if your compiler is good enough), but IMHO still a hack on a language defect.

<end digression>

What I propose is to solve the issue according to the original intent, which is also what users expect and all compilers that I know of already do. Or, in practice, we would have a rule that exists only as words in a standard document.

PS: I've sent a copy of this to Mr. Adamczyk to clarify an important doubt that occurred to me while writing this reply:

if no definition is provided for an integral static const data member is that member an object? Paragraph 1.8/1 seems to say no, and in fact it's difficult to think it is an object without assuming/pretending that a region of storage exists for it (an object *is* a region of storage according to the standard).

I would think that when no definition is required we have to assume that it could be a non-object. In that case there's nothing in 3.2 which says what 'used' means for such an entity and the current wording would thus be defective. Also, since the name of the member is an lvalue and 3.10/2 says an lvalue refers to an object we would have another problem.

OTOH the standard could pretend it is always an object (though the compiler can optimize it away) and in this case it should probably make a special case for it in 3.2/2.

Notes from the March 2004 meeting:

We sort of like this proposal, but we don't feel it has very high priority. We're not going to spend time discussing it, but if we get drafting for wording we'll review it.

Proposed resolution (October, 2005):

  1. Change the first two sentences of 6.3 [basic.def.odr] paragraph 2 from:

    An expression is potentially evaluated unless it appears where an integral constant expression is required (see 7.7 [expr.const]), is the operand of the sizeof operator (7.6.2.5 [expr.sizeof]), or is the operand of the typeid operator and the expression does not designate an lvalue of polymorphic class type (7.6.1.8 [expr.typeid]). An object or non-overloaded function is used if its name appears in a potentially-evaluated expression.

    to

    An expression that is the operand of the sizeof operator (7.6.2.5 [expr.sizeof]) is unevaluated, as is an expression that is the operand of the typeid operator if it is not an lvalue of a polymorphic class type (7.6.1.8 [expr.typeid]); all other expressions are potentially evaluated. An object or non-overloaded function whose name appears as a potentially-evaluated expression is used, unless it is an object that satisfies the requirements for appearing in an integral constant expression (7.7 [expr.const]) and the lvalue-to-rvalue conversion (7.3.2 [conv.lval]) is immediately applied.
  2. Change the first sentence of 11.4.9.3 [class.static.data] paragraph 2 as indicated:

  3. If a static data member is of const integral or const enumeration type, its declaration in the class definition can specify a constant-initializer which whose constant-expression shall be an integral constant expression (7.7 [expr.const]).