Take, for example, the following code:
#include <iostream>
#include <string>
int main()
{
print("Hello!");
}
void print(std::string s) {
std::cout << s << std::endl;
}
When trying to build this, I get the following:
program.cpp: In function ‘int main()’:
program.cpp:6:16: error: ‘print’ was not declared in this scope
Which makes sense.
So why can I conduct a similar concept in a struct, but not get yelled at for it?
struct Snake {
...
Snake() {
...
addBlock(Block(...));
}
void addBlock(Block block) {
...
}
void update() {
...
}
} snake1;
Not only do I not get warnings, but the program actually compiles! Without error! Is this just the nature of structs? What's happening here? Clearly addBlock(Block)
was called before the method was ever declared.
A struct
in C++ is actually a class
definition where its content are public
, unless specified otherwise by including a protected: or private: section.
When the compiler sees a class
or struct
, it first digests all the declarations within the block ({}
) before operating on them.
In the regular method case, the compiler hasn't yet seen the type declared.
C++ standard 3.4.1:
.4:
A name used in global scope, outside of any function, class or
user-declared namespace, shall be declared before its use in global
scope.
This is why global variables and functions cannot be used before an afore declaration.
.5:
A name used in a user-declared namespace outside of the definition of
any function or class shall be declared before its use in that
namespace or before its use in a namespace enclosing its namespace.
same thing just written again as the .4 paragraph explictely restricted its saying to "global", this paragraph now says "by the way, its true as well in namespeces folks..."
.7:
A name used in the definition of a class X outside of a member
function body or nested class definition29 shall be declared in one of
the following ways: — before its use in class X or be a member of a
base class of X (10.2), or — if X is a nested class of class Y (9.7),
before the definition of X in Y, or shall be a member of a base class
of Y (this lookup applies in turn to Y ’s enclosing classes, starting
with the innermost enclosing class),30 or — if X is a local class
(9.8) or is a nested class of a local class, before the definition of
class X in a block enclosing the definition of class X, or — if X is a
member of namespace N, or is a nested class of a class that is a
member of N, or is a local class or a nested class within a local
class of a function that is a member of N, before the definition of
class X in namespace N or in one of N ’s enclosing namespaces.
I think this speaks of all the code that does not stand in cpu executed code (eg declarative code).
and finally the interesting part:
3.3.7 Class scope [basic.scope.class]
1 The following rules describe the scope of names declared in classes.
1) The potential scope of a
name declared in a class consists not only of the declarative region
following the name’s point of declaration, but also of all function
bodies, brace-or-equal-initializers of non-static data members, and
default arguments in that class (including such things in nested
classes).
2) 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.
3)
If reordering member declarations in a class yields an alternate valid
program under (1) and (2), the program is ill-formed, no diagnostic is
required.
particularly, by the last point they use a negative manner to define that "any ordering is possible" because if re-ordering would change lookup then there is a problem. its a negative way of saying "you can reorder anything and its ok, it doesnt change anything".
effectively saying, in a class, the declaration is looked-up in a two-phase compilation fashion.
"why can I conduct a similar concept in a struct, but not get yelled at for it?"
In a struct
or class
definition you're presenting the public interface to a class and it's much easier to understand, search and maintain/update that API if it's presented in:
- a predictable order, with
- minimal clutter.
For predictable order, people have their own styles and there's a bit of "art" involved, but for example I use each access specifier at most once and always public
before protected
before private
, then within those I normally put typedef
s, const
data, constructors, destructors, mutating/non-const functions, const
functions, static
s, friend
s....
To minimise clutter, if a function is defined in the class, it might as well be without a prior declaration. Having both tends only to obfuscate the interface.
This is different from functions that aren't members of a class - where people who like top-down programming do use function declarations and hide the definitions later in the file - in that:
people who prefer a bottom-up programming style won't appreciate being forced to either have separate declarations in classes or abandon the oft-conflicting practice of grouping by access specifier
Classes are statistically more likely to have many very short functions, largely because they provide encapsulation and wrap a lot of trivial data member accesses or provide operator overloading, casting operators, implicit constructors and other convenience features that aren't relevant to non-OO, non-member functions. That makes a constant forced separation of declarations and definitions more painful for many classes (not so much in the public interfaces where definitions might be in a separate file, but definitely for e.g. classes in anonymous namespaces supporting the current translation unit).
Best practice is for classes not to cram in a wildly extensive interface... you generally want a functional core and then some discretionary convenience functions, after which it's worth considering what can be added as non-member functions. The std::string
is an often claimed to have too many member functions, though I personally think it's quite reasonable. Still, this also differs from a header file declaring a library interface, where exhaustive functionality can be expected to be crammed together making a separation of even inline
implementation more desirable.