1

As you will see, I am not very confident on my understanding of what happens when mixing pointers const and overload. So following on questions such as this one, I tried to explicit all cases in the following piece of code.

My question is : did I miss some cases of interactions between addresses, pointers, const pointers, and the various prototypes that may overload each other ?

//g++  7.4.0
#include <iostream>
using namespace std;

// ---------- which legal overloads ?
/* 
void f_012a( int* pi ){ cout << "pi"; }
//void f_012a( int * const pi ){ cout << "pci"; } //error: redefinition of ‘void f_012a(int*)’
void f_012a( const int * pi ){ cout << "pci"; }
//void f_012a( const int * const  pi ){ cout << "cpci"; } //error: redefinition of ‘void f_012a(const int*)’
*/

// ---------- overloads declaration order impact ?
void f_01( int* pi )       { cout << "pi";  }
void f_01( const int * pi ){ cout << "pci"; }
void f_10( const int * pi ){ cout << "pci"; }
void f_10( int* pi )       { cout << "pi";  }

int main(){
    cout << "/** trace\n";
    {
        cout << "-------- pointers and const\n";
        int* pi                = new int(1);
        int * const pci        = new int(2);
        const int * cpi        = new int(3);
        const int * const cpci = new int(4);
        cout << "pi -> " << *pi  << " / pci -> " << *pci << " / cpi -> " << *cpi << " / cpci -> " << *cpci << "\n"; 

        *pi  = 5;
        cout << "pi -> " << *pi  << " / pci -> " << *pci << " / cpi -> " << *cpi << " / cpci -> " << *cpci << "\n"; 
        pi   = new int(6);
        cout << "pi -> " << *pi  << " / pci -> " << *pci << " / cpi -> " << *cpi << " / cpci -> " << *cpci << "\n"; 

        *pci  = 7;
        cout << "pi -> " << *pi  << " / pci -> " << *pci << " / cpi -> " << *cpi << " / cpci -> " << *cpci << "\n"; 
        //pci   = new int(8); //error: assignment of read-only variable ‘pci’

        //*cpi  = 8;
        cpi   = new int(8);  //error: assignment of read-only location ‘* cpi’
        cout << "pi -> " << *pi  << " / pci -> " << *pci << " / cpi -> " << *cpi << " / cpci -> " << *cpci << "\n"; 

        //*cpci  = 9;          //error: assignment of read-only location ‘* cpi’
        //cpci   = new int(9); //error: assignment of read-only variable ‘cpci’
    }
    
    {
        cout << "-------- pointers, const, and overload\n";
        int i                  = 1;
        int* pi                = new int(2);
        int * const pci        = new int(3);
        const int * cpi        = new int(4);
        const int * const cpci = new int(5);
        cout << " i : " << i << " / pi -> " << *pi  << " / pci -> " << *pci << " / cpi -> " << *cpi << " / cpci -> " << *cpci << "\n"; 
    
        cout << "var\tf_01\tf_10\n"; 
        cout << "&i\t"; f_01(&i); cout<<"\t"; f_10(&i); cout<<"\n"; 
        cout << "pi\t"; f_01(pi); cout<<"\t"; f_10(pi); cout<<"\n"; 
        cout << "pci\t"; f_01(pci); cout<<"\t"; f_10(pci); cout<<"\n"; 
        cout << "cpi\t"; f_01(cpi); cout<<"\t"; f_10(cpi); cout<<"\n"; 
        cout << "cpci\t"; f_01(cpci); cout<<"\t"; f_10(cpci); cout<<"\n"; 
    }
        
    cout << "*/\n";
    return 0;
}

/** trace
-------- pointers and const
pi -> 1 / pci -> 2 / cpi -> 3 / cpci -> 4
pi -> 5 / pci -> 2 / cpi -> 3 / cpci -> 4
pi -> 6 / pci -> 2 / cpi -> 3 / cpci -> 4
pi -> 6 / pci -> 7 / cpi -> 3 / cpci -> 4
pi -> 6 / pci -> 7 / cpi -> 8 / cpci -> 4
-------- pointers, const, and overload
 i : 1 / pi -> 2 / pci -> 3 / cpi -> 4 / cpci -> 5
var f_01    f_10
&i  pi  pi
pi  pi  pi
pci pi  pi
cpi pci pci
cpci    pci pci
*/
chetzacoalt
  • 145
  • 10
  • 2
    From the caller's view the parameters `int *` and `int * const` are equal, you want to call the function with an `int *`. The second version only gives you the information, that the function does not change the pointer, which you do not care, as it is a copy of your pointer. – mch Oct 15 '20 at 08:14
  • what is explained in the Q&A you link applies in general. Unless `T` is a reference `void foo(const T)` and `void foo(T)` declare the same function. Pointers are not that special as you might think, hence it is not clear what you are looking for as answer. – 463035818_is_not_an_ai Oct 15 '20 at 08:43

1 Answers1

1

The real issue at play here is with understading the "right-to-left" rule regarding types. It is slightly confused by the existence of the "special case of leftmost const" in which const char is equivalent to char const.

In Nicolai Josuttis' book "C++ Templates" he makes the case of preferring "const right" to avoid using the special case, so the right-to-left rule is made consistent. Another advocate of this is Dan Saks in this video "East const but constexpr West" - https://www.youtube.com/watch?v=z6s6bacI424

There are a number of articles which explain the right-to-left rule and there is a cdecl tool which describes C types in plain English. See How do you read C declarations? and https://www.codeproject.com/Articles/7042/How-to-interpret-complex-C-C-declarations

There is also a web-based cdecl: https://cdecl.org/

Note also there are differences between passing parameters by value and by reference. When you pass a pointer to a method by value, you're essentially giving it a copy and it's up to the method to declare whether or not it will mutate the copy. When you pass a parameter by reference, you are sharing the variable, so a const reference cannot be mutated by the method.


The compiler's task is to choose the appropriate overload, which the linker connects according to the function's mangled name. This is how overloads work in the binary context.

Interestingly, in the Visual-C++ compiler, the name mangling is different when a pass-by-value parameter is const. This example looks at the 8 combinations of using a pointer-to-int, pointer-to-int-const, passing by value (const and not), passing by reference (const and not).

Note that if the methods are all called f_01 the compiler complains because it's presented with multiple possible options and cannot resolve the ambiguity, since you can pass a mutable-value to a const-value method.


// These do not affect the value of the pointer in the caller
void f_01(int* pi) 
{
    wprintf(L"%s\n", _CRT_WIDE(__FUNCDNAME__));

    // can mutate *pi and can increment the pointer
    *pi = 1;
    pi++;
}

void f_02(int* const pi)
{
    wprintf(L"%s\n", _CRT_WIDE(__FUNCDNAME__));

    // can mutate *pi but cannot increment the pointer
    *pi = 1;
    //pi++;
}

void f_03(int const* pi) 
{
    wprintf(L"%s\n", _CRT_WIDE(__FUNCDNAME__));

    // cannot mutate *pi but can increment the pointer
    //*pi = 1;
    pi++;
}

void f_04(int const* const pi)
{
    wprintf(L"%s\n", _CRT_WIDE(__FUNCDNAME__));

    //*pi = 1;
    //pi++;
}

// These could affect the value of the pointer in the caller
void f_05(int*& pi) 
{
    wprintf(L"%s\n", _CRT_WIDE(__FUNCDNAME__));

    *pi = 1;
    pi++;       //this will mutate the pointer in the caller
}

void f_06(int const*& pi) 
{
    wprintf(L"%s\n", _CRT_WIDE(__FUNCDNAME__));

    // cannot mutate the value addressed by the pointer -> *pi = 1;
    pi++;  //this will mutate the pointer in the caller
}

void f_07(int* const& pi) 
{
    wprintf(L"%s\n", _CRT_WIDE(__FUNCDNAME__));

    *pi = 1;
    // cannot do pi++;
}

void f_08(int const* const& pi) 
{
    wprintf(L"%s\n", _CRT_WIDE(__FUNCDNAME__));

    // cannot do *pi = 1;
    // cannot do pi++;
}

// --------------------------------

void test_f()
{
    int const constInt = 9;
    int mutableInt = 10;
    int const* pConstInt{ &constInt };
    int* pMutableInt{ &mutableInt };

    f_01(pMutableInt);
    f_02(pMutableInt);
    f_03(pConstInt);
    f_04(pConstInt);

    f_07(pMutableInt);
    f_08(pConstInt);
    // do these last!
    f_05(pMutableInt);
    f_06(pConstInt);

    // output is:
    //? f_01@@YAXPAH@Z
    //? f_02@@YAXQAH@Z
    //? f_03@@YAXPBH@Z
    //? f_04@@YAXQBH@Z

    //? f_07@@YAXABQAH@Z
    //? f_08@@YAXABQBH@Z
    //? f_05@@YAXAAPAH@Z
    //? f_06@@YAXAAPBH@Z


Taking this a step further, here we demonstrate that you can pass the mutable value to the const methods. Note the exception with the mutable reference to const pointer - in this case the parameter cannot be converted:

    // here we demonstrate that you can pass the mutable int to the const int methods
    // - which is why the overload ambiguity would occur
    f_01(pMutableInt);
    f_02(pMutableInt);
    f_03(pMutableInt);
    f_04(pMutableInt);

    f_07(pMutableInt);
    f_08(pMutableInt);
    f_05(pMutableInt);
    //f_06(pMutableInt); - this is not allowed


So what's the point of all of this? Essentially only use overloads when there is no possibility of ambiguity. Personally I avoid using them.

Den-Jason
  • 2,395
  • 1
  • 22
  • 17