14

Sayeth the C++ standard:

The point of declaration for a name is immediately after its complete declarator and before its initializer (if any)... [basic.scope.pdecl]

That is, a variable is in scope, and can be referred to, in the context of its own initialization expresion.

As far as I can tell, you can do the following types of things with it:

  1. int x = x, which is well-formed but meaningless.
  2. void* p = &p, which is cute but useless.
  3. std::any a {&a}, the C++17 version of #2.
  4. MyClass m {std::move(m)}, the C++11 version of #1 and probably UB somehow.
  5. MyClass m {myFunc(m)}, with a function which takes your uninitialized object, I guess records it somewhere? and returns some value back so the constructor can have a go.

#1-4 are useless, of course. It seems like one could construct an interface where #5 made some sense, but I can't see it being the most straightforward way of accomplishing anything. Since the new variable is not yet initialized when the initializer is evaluated, it's useless/illegal to read its value, and its address isn't generally going to be important to its initialization.

(One could make a slightly stronger case for leaving the point of declaration until after the initializer: int avg = avg(a,b,c). That's not good code, and it's not essential for anything, but it would make more sense than void* p = &p.)

But it's not just about use cases. In general, C++ takes pains to prevent access to an uninitialized object. For instance, it sets up an object's vtable one base class at a time: if D inherits from C inherits from B, during C's constructor virtual methods will be dispatched to C's implementations, not D's. The common situations where one can peek at an uninitialized object are this situation, and (more commonly problematic) the use of this in a member initializer expression.

So I can't see a use for bringing a name in scope before its initializer, and I can see a clear rationale (which Stroustrup would also have seen) for delaying it until after the initializer. Given that, is there a clear point to C++'s chosen behavior?

Sneftel
  • 40,271
  • 12
  • 71
  • 104
  • 13
    I'm not saying this was a rationale for C, but `int *p = malloc(5 * sizeof(*p));`. – chris Oct 21 '20 at 10:23
  • Interesting question. I think that the sentence referenced by the standard does not limit declarations only to variables. You can easily and legally declare a function `template auto foo() -> decltype(T::foo());` I'm not sure where this might be useful, but it's valid. – Constantinos Glynos Oct 21 '20 at 10:36
  • 1
    `int* arr = malloc( 10 * sizeof(*arr) );` is a valid use case. In Windows API you sometimes store size of a struct inside the struct. So, it does have its use cases. – Aykhan Hagverdili Oct 21 '20 at 10:49
  • @AyxanHaqverdili Chris mentioned the same thing. In the case of the winapi, though, all example code I've seen uses `sizeof(TYPE)`, not `sizeof(obj)`. Not an essential use case, except if needed for backwards compatibility. – Sneftel Oct 21 '20 at 10:59
  • @Sneftel what do you mean it's only needed for backwards compatibility? If you're writing win32 code today, you still do something similar. I like `sizeof(obj)` as you don't need to repeat the type name. I am not arguing that the rules are good. I am just saying that it does have some use cases. – Aykhan Hagverdili Oct 21 '20 at 11:37
  • I meant, it isn't something that you wouldn't otherwise be able to do, but might be useful so that legacy C code would stand more of a chance of compiling as C++ code. – Sneftel Oct 21 '20 at 11:50

1 Answers1

17

its address isn't generally going to be important to its initialization.

template <class T>
struct Node
{
  Node *prev, *next;
  std::optional<T> data;
};

template <class T>
struct List
{
    Node<int> sentinel {&sentinel, &sentinel};
    // methods ...
};

This is a totally legitimate and useful initialisation of a sentinel-based doubly linked list.

Any graph-like data structure can have self references, which can be implemented by passing pointers or references to an objects as parameters to its own constructor. There is no reason to disallow those.

If you are worried about undefined behaviour inherent in int x = x;, just enable warnings (warning: shameless plug) as you should have done anyway. Compilers are now reasonably good at catching this stuff.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
  • Good point about more complex self-references. FWIW, though, GCC -Wall -Wextra accepts `MyClass m{std::move(m)}` without any complaints. – Sneftel Oct 21 '20 at 10:40