Home > Enterprise >  Why don't const parameters need to be initialized when they're defined?
Why don't const parameters need to be initialized when they're defined?

Time:01-18

#include<iostream>
using namespace std;
int add(const int a, const int b)
{
    return a b;
}

Why is the code above correct? I think the const variables should be initialized when they are defined in the formal parameters list.

CodePudding user response:

I think the const variables should be initialized

They are certainly initialized. Every call to this function provides values for these parameters, which initializes them. Guaranteed. Every time. And it doesn't matter if they are const, or not. They'll still get initialized every time this function gets called. There are no other alternatives, that's simply how C works on a fundamental level.

when they are defined in the formal parameters list.

This is, technically, not a definition. This is a declaration.

You will find that the same C keyword will often mean (sometimes slightly, sometimes radically) different things, depending on its context.

When the const keyword gets used to declare an object in global scope, the object must certainly be initialized, to something:

const int a;

If this is a global variable definition, then this is ill-formed, and it won't compile.

But function parameters are not global variables.

CodePudding user response:

'const' here is a promise that the function will not modify those values. The values passed in do not have to be constants.

CodePudding user response:

Lexical Environments

A lexical environment is a name ⟶ object lookup. That is to say, I can give something a name (and a type) and associate it with some object value.

int x = -7;           /*
↑   ↑    ↑
↑   ↑    object value
↑   name
type                  */

std::string s = "Hello world!";
//          ↑      ↑
//          ↑      object value
//          name

There are also times when the name ⟶ object association can be modified. With normal variables, we can assign a value either at the time of creation:

int x = -7;         // associated with a value at creation

or at some other time:

string name;        // associated with a default value
name = "Bon-Jovi";  // associated with a new value

In C and C we can tell the compiler that the name ⟶ object association cannot be changed when we use the keyword const:

const x = -7;
x = 42;             // compile error: cannot change x’s value

The degree to which the constness of a value is permanent depends on various factors not discussed here, but the idea is simply that you are telling the compiler and, importantly, users of your code that you guarantee not to try to change things marked const.

Also, this is a vast simplification of how things work underneath, but it is how the language views the universe, and how you, the programmer generally should as well.


Formal Argument Names •vs• Actual Argument Values

Now some more vocabulary. A function has formal argument names (or formal parameter names, but in C and C we use the word “argument” instead of parameter), which is to say, we are telling the compiler what names we want to use when a local lexical environment is created for the function.

When we invoke (or call) a function, we supply actual argument values. These actual values are duplicated and the duplicate values are associated with the formal argument names in the function’s new, local lexical environment.

An example:

void f( int x )            // formal argument name == “x”
{
  std::cout << x << "\n";  // print the integer value associated with “x” in
}                          // f()’s current (local) lexical environment

Here we have defined code that behaves with a locally-associated (local to the function) lexical name — in this case, x.

We invoke the function later:

int airspeed_velocity_of_unladened_sparrow = 11 /* m/s */;
int number_thou_shalt_count_to = 3;

f( airspeed_velocity_of_unladened_sparrow );  // actual argument value = 11
f( number_thou_shalt_count_to );              // actual argument value = 3
f( -7 );                                      // actual argument value = -7

Each time the function is invoked, a new lexical environment is created for the function, with the formal argument names associated with duplicates of the actual argument values.

Once the function finishes its business, its local lexical environment is destroyed and program control returns to where the function was invoked.

This idea of associating (or binding) values to names is exactly what a lexical environment is about, and how functions work. It would be really, really pointless if I couldn’t re-use f() with different values of its x each time I invoke it.


Call by Value •vs• Call by Reference

So far we have described call-by-value semantics: a local copy of an object’s value is created and bound to a local name.

In C , however, it is possible to create a reference. A reference is an alias for another, extant object value. In other words, we can have multiple names in our lexical environment referring to the same object value!

int   x = 12;  // x ⟶ integer 12
int & y = x;   // y ⟶ that ↑ same integer
y = 93;        // same as: x = 93

Or, to say it in yet another way, the integer object value 12 is now associated with two names (x and y), either of which can be used to access the integer object.

A reference argument does the same thing, except the function’s local lexical environment is created with an alias name for a value in the caller’s lexical environment!

void f( int & x )
{
  x  = 1;  // `x` is an alias for... what?
}

int num_birds_in_hand = 0;
f( num_birds_in_hand );  // f()-local `x` == `num_birds_in_hand`
std::cout << x << "\n";  // prints 1!

In other words, when we bind (or associate) f()’s local x to an object, we simply bind it to the same object that num_birds_in_hand is bound to.

Or, to say it in yet another way, the integer object value 0 is now associated with two names (num_birds_in_hand and x), either of which can be used to access the integer object.

As long as the f()’s local lexical environment exists, the integer associated with the name num_birds_in_hand is also associated with the name x.

(And again, when the function terminates, its lexical environment is destroyed, and num_birds_in_hand no longer has any aliases.)

And, for the beauty of it all, each time we invoke f() we can initialize its local lexical environment with x aliasing a different object value.

int I_am_different = 37;
f( I_am_different );
std::cout << I_am_different << "\n";  // prints “38”

Const References

This reference thing can cause a problem. Sometimes we want a some kind of promise that my num_birds_in_hand will not be modified, even if I give out an alias to it.

This is what a const formal argument is for. By marking the formal argument with const, we promise that when the function is invoked and a local lexical environment is created, it will not be used to change the actual argument value.

void g( const int & x )  // const == promise not to modify x’s object value
{
  x  = 1;                // compile error: cannot change x’s value
}

The purpose of a reference argument is twofold: allow a function to modify a value in the caller’s lexical environment, and avoid having to make a copy of something expensive (or non-copyable). For example, fstream objects in the Standard Library cannot be copied. Hence we can only pass them as references. You see this with insertion operator overloads:

std::ostream & operator << ( std::ostream & outs, const my_type & value )
{
  // serialize whatever my_type object may be (possibly something large
  // or non-copyable) and output it to the `outs` stream.
  // Once done, we return the argument stream:
  return outs;
}

my_type quux;
std::cout << quux << "\n";

Const Values

In the case of the code you showed us:

int add( const int a, const int b )

there really isn’t much point in marking a and b as const — no one cares what add() does with its local copies of the actual argument values.

(Sometimes the person writing the function’s code cares, so he or she will mark the formal argument name with const. But no one using the function cares.)

(That said, sometimes people actually do care. In the example code it does tell the caller that a and b won’t have something weird done to them before using their values. But I would argue that that just means that the caller knows too much about the function’s internal workings. As far as I should know, when invoking a function, how the function does its job should be unknown magic to me.)

Ultimately, however, it does not matter whether pass-by-value arguments are tagged const or not.


tl;dr

Formal argument names are initialized with actual argument values every time a function is invoked — a new lexical environment, local to that function, is created, binding the formal argument names to actual argument values, before the function’s code is executed. (Once the function returns, its lexical environment is destroyed and program control is returned to the caller.)

  •  Tags:  
  • c
  • Related