Of mutability, volatility, portability, and other things that go bump in the night...
Copyright © 2001 Robert H. Schmidt
To const or Not to const
Q
C++ Standard subclause 8.3.5/3 makes clear that the following declares the same function twice:
void f(int x); // same function, // const is ignored void f(const int x);Obviously, then, it doesnt matter what order you put the declarations in:
// still the same function // declared twice void f(const int x); void f(int x);But consider what happens when the second declaration is also a definition:
// case 1: x is declared non-const // in f1s declaration and const // in its definition void f1(int x); void f1(const int x) { x = 1; // modify x } // case 2: x is declared const // in f2s declaration and non-const // in its definition void f2(const int x); void f2(int x) { x = 2; // modify x }Most of my compilers reject case 1 and accept case 2, but one does the reverse. Which is correct, and why?
Thanks Ray DiTutto
A
My reading of the same paragraph (8.3.5/3) suggests that case 2 is okay while case 1 is not. From that paragraph:
All declarations for a function with a given parameter list shall agree exactly both in the type of the value returned and in the number and type of parameters...
The type of a function is determined using the following rules...
After producing the list of parameter types, several transformations take place upon these types to determine the function type.
Any cv-qualifier modifying a parameter type is deleted [Example: the type void(*)(const int) becomes void(*)(int) end example] Such cv-qualifiers affect only the definition of the parameter within the body of the function; they do not affect the function type.
In case 1, the function f1 has two declarations:
void f1(int x) void f1(const int x)According to 8.5.3/3, the compiler ignores the const when determining f1s type in each declaration. The compiler thus interprets the two declarations as
void f1(int x) void f1(/*const*/ int x)The two f1 declarations do not represent f1 overloads, but rather multiply declare the same f1 function.
As the subclause tells us, the const does affect the x parameters definition within the body of f1. Because x is const in that body, the statement x = 1 is in error.
The same analysis applies to case 2. There, the compiler considers f2 to be multiply declared as
void f2(/*const*/ int x) void f2(int x)In the context of f2s body, the parameter x is (non-const) int, and the assignment x = 2 is allowed.
In general, a parameter serves two distinct roles, depending on context:
- Within a function declarator, the parameter contributes to the functions signature and declared type according to 8.3.5/3. In this context, the parameters top-level const and volatile qualifiers are ignored.
- Within a function body which grammatically is a compound statement the parameter is a local-scope object declared before the bodys opening curly brace ({). This parameter object happens to initialize from a function-call argument; otherwise, it behaves like any other local-scope object. In particular, it retains the significance of top-level const and volatile qualifications.
Although they dont work in C++, I think the old K&R-style function definitions actually make the distinction more clear. In the C function definition
int f(a, b, c) int a; int b; int c; { /* ... */ }the parameter names appear twice, both as part of the functions declarator, and as a list of local-scope objects declared just before the opening curly brace. This presentation reinforces the parameters dual purpose [1].
Mon Dieu! An Actual C Question
Q
Bobby,
My question is about Standard C, not C++.
Consider this code:
void f1(void *p); void f2(volatile void *p) { f1(p); }It compiles just fine with version 4.2 of my embedded compiler, Diab Data C. However, the newer version (4.3) issues the following error message:
'f1' : cannot convert parameter 1 from 'volatile void *' to 'void *'I tried the code on two PC compilers (Visual C 5 and Borland C 5.5). Both produce warnings, but complete the compilation.
What behavior is right from the C Standard point of view?
Diabs technical support person suggested I use the option flag -Xmismatch-warning to avoid my problem. But this option essentially eliminates type checking on pointers. With that flag on, the compiler allows the following code without errors:
void f3(short *ps) { long *pl = ps; } typedef struct { int m1; } st1; typedef struct { int m2; } st2; void f4(st1 *p1) { st2 *p2 = p1; } void f5(long l) { long *pl = l; }I was amazed to find out that VC5 also compiles this code. BC5.5 compiles f3 and f4, but produces on error on f5.
For years Ive thought that Standard C only allows three types of implicit pointer conversions (now I put this complex const/volatile issue aside):
- void * to anytype *
- anytype * to void *
- 0 to anytype *
Was I wrong ?
I know, there are two questions instead of one, but I found these questions closely related.
Regards Michael Shatz
A
Your first code piece should not compile, as it involves an implicit conversion from void volatile * to void *. The conversion requires a reduction in qualification of the pointed-to type, which the C standard will not permit without a cast [2]. If you have trouble understanding this restriction, mentally replace the qualifier volatile with the qualifier const.
Your second code piece the one with fs 3 through 5 also should not compile. In particular, you ought to see diagnostics for these three lines:
- long *pl = ps;
- st2 *p2 = p1;
- long *pl = l;
which involve three different non-standard implicit conversions:
- between pointers to different integer types
- between pointers to different aggregate types
- from an integer type to a pointer type
On your final question: the C standard allows several kinds of pointer conversions [3]. Of those, the allowed implicit conversions are
- from T * to qualified T *
- from unqualified T * to void *
- from void * to T *
- from 0 to T *
where T is an incomplete or object type. Excepting the bits about qualification, this list matches your years-long belief.
Headbangers Ball
Q
Ive been trying to find a way to make data members read/write within member functions, but read-only within non-member and friend functions, without having to write get functions so far to no avail.
First I naively thought of a template class having a public conversion operator that returns const T&; but this requires that I first declare a const ref to the class, then initialize it by the conversion function (which then becomes redundant). This method doesnt make private members visible anyway.
I next tried declaring two structs:
struct A { const int i; const float f; }; struct B { int i; float f; }; union C { public: A a; private: B b; };But this forces me to write a.i or b.i when I want the public const or private non-const versions of the member. Furthermore, according to my compiler, unions cant be inherited.
I also tried having a class with a public, nameless union; but again, that requires extra typing to select the union member.
Not giving up altogether, I tried to make a macro that would produce easy-to-type access functions for me:
#define member(type, name)\ public:\ const type &name() const\ {\ return name;\ }\ private:\ type namesuch that typing
member(int, i); member(float, f);would produce
public: const int &i() const { return i; } private: int i; public: const float &f() const { return f; } private: float f;Unfortunately one cant use the same name for a variable and a function, so Id have to make the macro name the functions _i and _f. This is much less satisfying than the original goal Id set out to achieve.
Finally, I tried a macro that would create nameless unions within my class, such as,
union { public: const int _i; private: int i; };but according to my compiler, nameless unions cannot have private members.
Im not only frustrated by now, but even resentful; I cant, for the life of me, justify the invisibility of private and protected data members mandated by the language.
In what way would read-only access by default be a violation of encapsulation? Quite to the contrary it seems to me that invisibility causes the biggest violation of encapsulation of all: programmers making data members public to avoid having to write get functions! I often have to work with other peoples code, and much of my time is spent writing the get functions they couldnt bother writing themselves.
Any help would be enormously appreciated.
Thanks in advance Dan Watkins
A
While I would normally whittle down such a long question, I want to show others how pursuit of a seemingly simple goal can lead to so many dead ends and how this language can drive anyone to the brink of despair.
If Im reading your intent correctly, youre looking to allow
class X { public: void f() { i = 6; // OK } int i; }; int main() { X x; x.i = 6; // error }In effect, you want i to behave as non-const within X, and const outside X.
Im sorry to say that C++ offers no direct way to get what you want. About the closest I can easily come is
class X { public: X() : i_(0), i(i_) { } void f() { i; // OK i = 6; // error i_ = 6; // OK } int const &i; private: int i_; }; int main() { X x; x.i; // OK x.i = 6; // error x.i_ = 6; // error }Compared to your ideal, this method suffers one deficiency: the member function f must reference the data member as i_ instead of i for writeable access. Otherwise, this solution has the notational convenience and selective protection you want.
Erratica
Erratum the first: my October 2000 column item Ambiguity Rides Again contains a bug. The statement
B2 b2 = val;wont compile with the definition of B provided. That statement is a remnant from an earlier code version I was testing, and shouldnt be there.
Erratum the second: several Diligent Readers question my solution for the December 2000 item Nested Functions. In that item, I explore several possible answers to Mike Retondos questions about functors. One of those answers amounts to
typedef void (*callback)(); void test(callback f) { f(); } struct X { operator callback(); }; int main() { X x; test(x); return 0; }x converts itself to tests parameter type (callback)) through a conversion operator (X::operator callback). In effect, the object x acts as if it were really a function taking no parameters and returning void.
I never gave the implementation for the conversion operator. In that vacuum, some readers assumed that Id mimic Mikes original solution via
struct X { operator callback() { printf("test"); } };This isnt the solution I had in mind, for two reasons:
- It engenders undefined behavior: because the operator isnt actually returning anything, the eventual call f() blasts into the Delta Quadrant.
- The printf call occurs during the conversion to the function pointer, not during the call through the function pointer. Contrast this to Mikes original, which calls printf during the function-like call through operator().
Other readers thought I really meant
struct X { operator callback() { printf("test"); return operator callback; } };which wont compile, since a function cant return itself [4].
What I actually had in mind is
struct X { operator callback() { return f; } private: static void f() { printf("test"); } };which returns a proper function pointer, and traces the call through that pointer. (A couple of readers recommend essentially this same implementation.)
Such a solution allows the same behavior for all X objects. A more sophisticated solution allows distinct per-object behavior:
struct X { operator callback() { instance_ = this; return f; } private: static void f() { printf("%p", (void *) instance_); } static X const *instance_; };at the cost of a static data member and a restriction on X object usage: because the same instance_ is shared by all X objects, only one X-to-callback conversion can be in play at a time.
Notes
[1] Not that Im advocating this style, mind you. For pedagogical purposes, I think it has some merit; but overall, I recommend you favor prototype-style function declarators instead.
[2] C99 standard subclause 6.5.4/3.
[3] The whole retinue appears in subclause 6.3.2.3 of the C99 Standard.
[4] I explore the problem of self-returning functions in my July 1999 column item Chicken and Egg and October 1999 followup Counting Chickens.
Bobby Schmidt is a freelance writer, teacher, consultant, and programmer. He is also an alumnus of Microsoft, a speaker at the Software Development and Embedded Systems Conferences, and an original "associate" of (Dan) Saks & Associates. In other career incarnations, Bobby has been a pool hall operator, radio DJ, private investigator, and astronomer. You may summon him on the Internet via BobbySchmidt@mac.com.