By edwardlam0328


2019-06-09 14:04:22 8 Comments

I am learning the basics of C++ and OOP in my university now. I am not 100% sure how a function pointer works when assigning functions to them. I encountered the following code:

void mystery7(int a, const double b) { cout << "mystery7" << endl; }
const int mystery8(int a, double b) { cout << "mystery8" << endl; }

int main() {
    void(*p1)(int, double) = mystery7;            /* No error! */
    void(*p2)(int, const double) = mystery7;
    const int(*p3)(int, double) = mystery8;
    const int(*p4)(const int, double) = mystery8; /* No error! */
}

From my understanding, the p2 and p3 assignments are fine as the function parameters types match and const-ness is correct. But why don't the p1 and p4 assignments fail? Shouldn't it be illegal to match const double/int to non-const double/int?

3 comments

@Vlad from Moscow 2019-06-09 14:11:25

According to the C++ Standard (C++ 17, 16.1 Overloadable declarations)

(3.4) — Parameter declarations that differ only in the presence or absence of const and/or volatile are equivalent. That is, the const and volatile type-specifiers for each parameter type are ignored when determining which function is being declared, defined, or called.

So in the process of determining of the function type the qualifier const for example of the second parameter of the function declaration below is discarded.

void mystery7(int a, const double b);

and the function type is void( int, double ).

Also consider the following function declaration

void f( const int * const p );

It is equivalent to the following declaration

void f( const int * p );

It is the second const that makes the parameter constant (that is it declares the pointer itself as a constant object that can not be reassigned inside the function). The first const defines the type of the pointer. It is not discarded.

Pay attention to that though in the C++ Standard there is used the term "const reference" references themselves can not be constant opposite to pointers. That is the following declaration

int & const x = initializer;

is incorrect.

While this declaration

int * const x = initializer;

is correct and declares a constant pointer.

@edwardlam0328 2019-06-09 14:26:16

Thanks for the quick reply. This surely is some new concepts that I now grab on. But what happens when I try to pass (const int, double) to *p4? Will that in turn cause compilation error as the pointer points to a function with (int, double) parameters?

@Lightness Races in Orbit 2019-06-09 14:31:36

@edwardlam0328 No, the two are treated equivalently, per the rule Vlad quoted.

@Vlad from Moscow 2019-06-09 14:36:57

@edwardlam0328 Parameters are local variables of functions that are initialized with arguments uisng copy initialization. You can imagine it the following way. void f( int x ); const int i = 10; f( i ); void f( /*int x*/ ) { int x = i; /*...*/ } That is this declaration int x = i; is valid and a constant can be used as an initializer.

@Lightness Races in Orbit 2019-06-09 14:42:50

Your quoted standardese is about lookup, not about function types, no?

@edwardlam0328 2019-06-09 14:44:38

Thank you all for reminding me about pass by value! I forgot this key concept. So if I understand it right, because pass-by-value parameters create a local copy inside the function body, so it does not affect the original const-type variable passed, so this way we are sort-of bypassing the const restriction? And this is why declaring the function pointer as (const int, double) or (int, double) will not make a difference, correct?

@Lightness Races in Orbit 2019-06-09 14:45:39

@edwardlam0328 More or less, yes. The language designers didn't have to make it this way (and, after all, const on a value still has purpose and meaning inside the function body!). But they did so that the callsite doesn't have to care, I guess.

@Leushenko 2019-06-10 08:18:10

const on a parameter's value only has meaning inside the function's body, so the question is less "why are these compatible" and more "why is it permitted to qualify parameters at all". Although technically the same could be said for parameter names and those are useful for documentation/readability, so there's an argument to allow it.

@Davislor 2019-06-10 01:35:56

There is a situation where adding or removing a const qualifier to a function argument is a serious bug. It comes when you pass an argument by pointer.

Here’s a simple example of what could go wrong. This code is broken in C:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// char * strncpy ( char * destination, const char * source, size_t num );

/* Undeclare the macro required by the C standard, to get a function name that
 * we can assign to a pointer:
 */
#undef strncpy

// The correct declaration:
char* (*const fp1)(char*, const char*, size_t) = strncpy;
// Changing const char* to char* will give a warning:
char* (*const fp2)(char*, char*, size_t) = strncpy;
// Adding a const qualifier is actually dangerous:
char* (*const fp3)(const char*, const char*, size_t) = strncpy;

const char* const unmodifiable = "hello, world!";

int main(void)
{
  // This is undefined behavior:
  fp3( unmodifiable, "Whoops!", sizeof(unmodifiable) );

  fputs( unmodifiable, stdout );
  return EXIT_SUCCESS;
}

The problem here is with fp3. This is a pointer to a function that accepts two const char* arguments. However, it points to the standard library call strncpy()¹, whose first argument is a buffer that it modifies. That is, fp3( dest, src, length ) has a type that promises not to modify the data dest points to, but then it passes the arguments on to strncpy(), which modifies that data! This is only possible because we changed the type signature of the function.

Trying to modify a string constant is undefined behavior—we effectively told the program to call strncpy( "hello, world!", "Whoops!", sizeof("hello, world!") )—and on several different compilers I tested with, it will fail silently at runtime.

Any modern C compiler should allow the assignment to fp1 but warn you that you’re shooting yourself in the foot with either fp2 or fp3. In C++, the fp2 and fp3 lines will not compile at all without a reinterpret_cast. Adding the explicit cast makes the compiler assume you know what you’re doing and silences the warnings, but the program still fails due to its undefined behavior.

const auto fp2 =
  reinterpret_cast<char*(*)(char*, char*, size_t)>(strncpy);
// Adding a const qualifier is actually dangerous:
const auto fp3 =
  reinterpret_cast<char*(*)(const char*, const char*, size_t)>(strncpy);

This doesn’t arise with arguments passed by value, because the compiler makes copies of those. Marking a parameter passed by value const just means the function doesn’t expect to need to modify its temporary copy. For example, if the standard library internally declared char* strncpy( char* const dest, const char* const src, const size_t n ), it would not be able to use the K&R idiom *dest++ = *src++;. This modifies the function’s temporary copies of the arguments, which we declared const. Since this doesn’t affect the rest of the program, C doesn’t mind if you add or remove a const qualifier like that in a function prototype or function pointer. Normally, you don’t make them part of the public interface in the header file, since they’re an implementation detail.

¹ Although I use strncpy() as an example of a well-known function with the right signature, it is deprecated in general.

@M.M 2019-06-10 01:51:10

Note that the usage strncpy(buf, src, sizeof(buf)); fputs(buf, stdout); is incorrect in general, since the output of strncpy is not null-terminated if src does not fit in buf

@Davislor 2019-06-10 01:52:36

@M.M I could’ve written the example with memcpy(), but I think it serves its purpose here?

@Davislor 2019-06-10 02:05:49

@M.M Added a footnote.

@Lightness Races in Orbit 2019-06-09 14:28:29

There is a special rule for function arguments passed by value.

Although const on them will affect their usage inside the function (to prevent accidents), it's basically ignored on the signature. That's because the constness of an object passed by value has no effect whatsoever on the original copied-from object at the call site.

That's what you're seeing.

(Personally I think that this design decision was a mistake; it's confusing and unnecessary! But it is what it is. Note that it comes from the same passage that silently changes void foo(T arg[5]); into void foo(T* arg);, so there's plenty of hokey bullsh!t in there already that we have to deal with!)

Do recall, though, that this doesn't just erase any const in such an argument's type. In int* const the pointer is const, but in int const* (or const int*) the pointer is non-const but is to a const thing. Only the first example relates to constness of the pointer itself and will be stripped.


[dcl.fct]/5 The type of a function is determined using the following rules. The type of each parameter (including function parameter packs) is determined from its own decl-specifier-seq and declarator. After determining the type of each parameter, any parameter of type “array of T” or of function type T is adjusted to be “pointer to T”. After producing the list of parameter types, any top-level cv-qualifiers modifying a parameter type are deleted when forming the function type. The resulting list of transformed parameter types and the presence or absence of the ellipsis or a function parameter pack is the function's parameter-type-list. [ Note: This transformation does not affect the types of the parameters. For example, int(*)(const int p, decltype(p)*) and int(*)(int, const int*) are identical types. — end note ]

@M.M 2019-06-10 01:53:16

IMO it was a good decision -- whether or not the function implementation wants to modify the copied object is not something that the caller should have to know.

@Lightness Races in Orbit 2019-06-10 10:09:35

@M.M That's a "nice to have" at best, and one that creates a special case in the type system, leading to confusion like the OP's

Related Questions

Sponsored Content

9 Answered Questions

[SOLVED] Meaning of 'const' last in a function declaration of a class?

24 Answered Questions

[SOLVED] Set a default parameter value for a JavaScript function

37 Answered Questions

13 Answered Questions

[SOLVED] What is a smart pointer and when should I use one?

17 Answered Questions

[SOLVED] What is the difference between const int*, const int * const, and int const *?

8 Answered Questions

[SOLVED] How to convert a std::string to const char* or char*?

  • 2008-12-07 19:30:56
  • user37875
  • 876538 View
  • 854 Score
  • 8 Answer
  • Tags:   c++ string char const

32 Answered Questions

[SOLVED] What is the difference between const and readonly?

6 Answered Questions

[SOLVED] Inheriting constructors

6 Answered Questions

[SOLVED] What is meant with "const" at end of function declaration?

  • 2010-06-29 13:31:02
  • aPoC
  • 433311 View
  • 485 Score
  • 6 Answer
  • Tags:   c++ const

4 Answered Questions

Sponsored Content