3

I am using a function from an external library (here represented by the printer-function).
By chance, I noticed, that I can pass a class member from a const member function like done by the print()-function. Even if I compile using

g++ -Wall -Wextra -Wpedantic -Werror -o test test.cpp

I don't obtain an error.

This seems to be horribly wrong, because effectively I am modifying the object by calling a const member function. Why is the compiler not complaining? Here is the code:

#include <iostream>
using namespace std;

void printer(void* data) {
    static_cast<char*>(data)[0] = '1';
    cout << static_cast<char*>(data) << endl;
}

void setUp(char* data, int length) {
    for(int i=0; i<length; i++)
        data[i] = '0';
    data[length] = '\0';
}

class Test {
    private:
        char* data;

    public:
        Test(int length) {
            data = new char(length+1);
            setUp(this->data, length);
        }

        ~Test() {
            delete data;
        }

        void print() const {
            printer(this->data);
        }
};

int main() {
    Test T(3);
    T.print();
    return 0;
}

My best guess is, that the compiler only guarantees, that the pointer variable is not modified, i.e. not pointing to a different physical address after the function call. But I also noticed, that the compiler throws errors when I try something similar with a std::vector<char> instead of char* and a modified print-function:

void print() const {
    printer(this->myVector.data());
}

So if my guess is correct, why is it not possible to do something similar with the pointer obtained by data() of a STL vector?

Solomon Ucko
  • 5,724
  • 3
  • 24
  • 45
staxyz
  • 167
  • 5
  • 7
    If you cast to `void*` you're discarding any `const` flags that apply. You're stepping outside of the usual type-checking rules. – tadman Sep 14 '20 at 20:35
  • 1
    there is no const external function. – asmmo Sep 14 '20 at 20:36
  • Converting `std::vector` to something you can print will require a `0` entry at the end that vector or it's not a valid C string. – tadman Sep 14 '20 at 20:38
  • 1
    Unrelated: typo in the example: `data = new char(length+1);` almost certainly should be `data = new char[length+1];` – user4581301 Sep 14 '20 at 20:40
  • 1
    @tadman, There's no cast to `void*`, though. There's an implicit conversion going on, but that conversion won't discard `const`. (I should clarify that exactly what I mean is that it won't convert `const char*` to `void*`.) – chris Sep 14 '20 at 20:44

2 Answers2

5
  1. You have a char* data member.

  2. In the context of a const-qualified method, that means the member may not be mutated.

  3. The member is the pointer. Changing a pointer means changing where it points.

  4. Just to be clear, the effective qualified type inside the method is

     char * const
    

    and not

     const char *
    
  5. Which is all to confirm that you're absolutely right that

    My best guess is, that the compiler only guarantees, that the pointer variable is not modified, i.e. not pointing to a different ... address after the function call.

However, for the vector, you want to call data on something that again has effective qualified type std::vector<char> const& ... so you need the const-qualified overload of std::vector::data, and that returns const char*. This is what you expected in the first place, but it's not the same behaviour as the raw pointer.

The reason for the difference is that the vector was written to express ownership of that data, so a const vector should have const contents. Raw pointers may own the data they point at, but there's no way to express this, and no way to have different const behaviour for owning and non-owning raw pointers. This is one of the may reasons to avoid raw pointers where possible.

Useless
  • 64,155
  • 6
  • 88
  • 132
  • But shouldn't `std::vector::data` return also a `char* const`? The fact that is behaves differently seems a bit inconsistent. I know that there is no overload of `data()` that returns `char* const`; but wouldn't that be actually sufficient to satisfy the const-qualifier of `print()`? – staxyz Sep 14 '20 at 21:11
  • No, it's a method and I linked to its documentation. You can see what it returns. There'd be no point in returning a const pointer, because you're returning it by value anyway. Here, vector's behaviour is both what you implied you wanted in the first place, and more correct for something that owns the data. The ambiguity about whether a raw pointer owns the object it points at (and the fact that const owning raw pointers don't make the pointee const) is one of the reasons that raw pointers are broadly discouraged. – Useless Sep 15 '20 at 00:22
  • Thank you, that was an incredibly helpful answer. I think it would be worthwhile to copy your comment beginning from "Here, vector's behaviour..." to the end of your answer, as it clarifies the design choice in std::vector and why it is reasonable to be different from the raw pointer case. – staxyz Sep 15 '20 at 02:45
  • I didn't think that was what you were asking. Your question was about why the raw pointer _didn't_ have this behaviour, right? Not why the `vector` _did_? – Useless Sep 15 '20 at 11:48
  • Well, its not in the title, but the last sentence in my post is precisely that question. – staxyz Sep 17 '20 at 10:11
  • This is why it's recommended to ask _one_ question per question. You actually asked _why can I do X, why does the compiler not complain about X, and why can't I do Y_. The first two are about the semantics of raw pointers, and the last is about the semantics of a library class that was designed to behave differently to raw pointers. – Useless Sep 17 '20 at 10:59
1

This is a common way of allowing a const method to change data. The immutable thing in Test is the pointer address data; which is not altered by print. You are nonetheless free to mutate what the pointer is addressing.

Another way round a const method is to declare a member as being mutable. Arguably, ensuring an internal string attribute is e.g. null terminated for printing could be considered a legitimate use of mutable.

See this piece from Kate Gregory at CppCon 2017 where she provides an interesting example (a method is designed to be const but a cache is added later): https://youtu.be/XkDEzfpdcSg?t=854

Also see this question for a lively discussion on the topic: Does the 'mutable' keyword have any purpose other than allowing the variable to be modified by a const function?

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