Physical constness of a class
I'm studying "The C++ Programming Language" from Bjarne Stroustrup and he talks about logical and physical constness of a class.
The example of logical constness is something like:
class A {
int m;
void func() const { m++; } //forbidden
}
It's possible to bypass this with a cast, like:
class A {
int m;
void func() const { (A*) this)->m++; } //allowed
}
In his words, logical constness is
"an object that appears constant to its users."
and physical constness is
"stored in read-only memory"
As a note he says that
physical constness may be enforced by placement of an object in read-only memory only for classes without constructors
I didn't quite understand this statement. Could someone provide an explanation on how to enforce physical constness and开发者_StackOverflow why it does not work if the class has a constructor?
You received several answers already, but I believe most of them (if not all) miss the point.
To better understand the situation with constness in C++ (and also involve the concepts mentioned in the other answers) let's consider not two, but three possible levels of constness that can be encountered in a C++ program
- Hardware/OS level physical constness
- Language level physical constness
- Logical constness (also, of course, language level)
Hardware/OS level physical constness is the physical constness that other answers seem to be describing. It takes place when the object is placed in memory protected from being written: read-only (RO) memory. The protection can be implemented by hardware-provided means or by OS-provided means or by both. However, the C++ language itself does not separate this kind of constness into a distinctive category. The C++ language does not concern itself with such low-level matters. When the notion of physical constness arises in C++ context, it is usually referring to the next kind of constness.
Language level physical constness. This constness takes place simply when you declare an object as const
. The following objects are physical constants from the point of view of C++ language
const double d = 5;
const int i = 42;
const std::string str = "Hello World!";
const MyClass c;
Note, that it doesn't really matter whether these object are really placed in RO memory or not. The language says that any attempts to modify these objects will result in Undefined Behavior (UB), regardless of whether the memory is RO or not. Note also, that if you attempt to modify these objects, the manifestations of that UB are not limited to a mere program crash (if they are really in RO memory). For example, the compiler is free to assume that these objects never change and can optimize the code under that assumption, eliminating access to these objects in situations when it appears to be unnecessary. Because of this, even if the memory occupied by these objects is writeable and even if you manage to "successfully" modify them somehow, your code might still behave as if your modifications never took place. UB is UB. Anything can happen.
From the language point of view this kind of constness is, of course, intended to include the 1st kind.
Finally, to Logical constness. Logical constness in C++ is the constness of so called acces path to the object. Access path is the reference or the pointer that allow you to access to existing object indirectly. Consider this declaration
const MyClass* pc;
This is a pointer to const MyClass
type. Note, however: it doesn't really mean that the actual object this pointer is pointing to is a constant. It just let's you "see" it as a constant. The object can easily be either a constant
const MyClass c;
pc = &c;
or it might be a non-constant
MyClass nc;
pc = &nc;
In other words, having just that pointer p
you have a constant accss path to some object of type MyClass
. You don't know and (normally) don't need to know whether that object is really a constant. Since the access path that was given to you is constant, you have to treat that object as a constant. Of course, if you somehow know that the object on the other end of that access path is not a constant, you can legally cast away the constness of the access path
MyClass* p = const_cast<MyClass*>(pc);
and perform modifying operations on the object (of course, in general case it is not a good programming practice, but it has its valid uses). If the object on the other end of the path turns out to be a constant after all, the behavior will be undefined for the reasons described above.
Note, that the example in the original post is talking about exactly that. When you declare a method of class A
as const
, it simply means that the implicit this
parameter passed to that method will have type const A*
, i.e. it will provide a constant access path to the A
object. This is what I described above as logical constness. Note, again, that if the object was declared as, say, const A a;
, it is a language-level physical constant and modifying it as shown in the example is illegal, regardless of whether the object is residing in RO memory or not.
Now, to provide one final illustration of the above, consider the follwing declaration
const MyClass* const* const* const p = /* whatever */;
This declaration has 4 const
qualifiers in it. One of these qualifiers has a major qualitative diference from the others. It is the rightmost one. The rightmost const
declares the physical constness of object p
(constness of the pointer itself), while the remaining const
qualifiers declare logical constness of the objects they will be pointing to (constness of the access path).
Again, I believe that in his book Stroustrup meant to talk about the distinction between the 2nd and 3rd concepts of constness, not about the 1st, since the C++ language doesn't really separate 1st from the 2nd. Note, that the example says that the modification by casting away constness is "allowed", while the language specification clearly says that modifying the constants of the 2nd kind by this approach is immediately illegal.
The constructor writes into the object as part of the construction process, but this doesn't work if the memory for the object is readonly.
Physical const
only works for objects that can be initialized at compile time, and thus have no need for a constructor.
The creation of readonly memory is not something you can do explicitly in C++, but many compilers have extensions that allow you to do this, and most modern compilers will put code and some data in readonly memory if the CPU architecture allows for this.
In MSVC, you can force an initialized global variable to be stored in read-only memory by placing it in a section marked as read but not write like this
#pragma section("rosec",read)
__declspec(allocate("rosec")) int j = 0; // this will be in a readonly data segment.
You will need help from the operating system.
On Linux, you can call mprotect to render a page read-only. On Windows, there's a corresponding API.
It's up to you to come up with an allocator that worries about page boundaries and object boundaries.
If your object has a constructor, it can't be put into a read-only section by the linker, since that read-only-ness would stop the constructor from constructing.
If you have a POD class, and you declare an object at file scope to be const, it might end up in a read-only region.
For certain types of object the compiler will build the object at compile time and place it in read only memory. As a result of this being done at compile time this can only happen if there is no runtime work to be done thus you can not have constructors.
struct A
{
int x;
float y;
};
A const a = {5,6.5}; // This object can be built at compile time and placed in read only memory.
To really enforce physical constness, you can always memory-map a write protected USB drive and/or CD-R. This will allow you to create pointers to your physically const instances. Overkill, but I thought I'd mention it.
My understanding, and experience, is that only data in your code segment (executable machine code), is read-only on modern architectures (unless this is disabled, for self-modifying code / JVM type stuff). Your compiler will tend to cheat, flopping between literal substitution in the code segment, and relying and read/write RAM for pointer operations (always overridden with a type cast).
So the short of it is, one suggestion so far mentions marking memory pages manually via your operating system. That's likely your only viable option for industrial strength physical const. That or my suggestion of hardware. Your compiler won't do this for you.
精彩评论