A static_cast is not always just a pointer adjustment
blogs.msdn.microsoft.com
blogs.msdn.microsoft.com
Somewhat related stuff that might be interesting:
dynamic_cast in C++: http://mentorembedded.github.io/cxx-abi/abi.html#rtti
Golang's interface casts (also used by Swift I believe): http://research.swtch.com/interfaces
Speculation for interface casts in Java: https://wiki.openjdk.java.net/display/HotSpot/PerformanceTec...
So, on the one hand there is cast, and on the other hand there is data type conversions. Cast is usually documented as just a cast where the meaning of that word is not in any dictionary and is not well explained otherwise. So, with a cast, often are getting a data conversion of some kind, but are usually missing the documentation to know just what.
My best explanation: C wanted some cases of strong typing, e.g., so that pointers could have data types and, thus, the relatively heavy use of pointers would cause less trouble. But a cast was an instance of overriding the usual strong typing. So, really, cast was presented as a way to override strong typing, but the issue of just how the data conversions would be done was left poorly or undocumented.
When I do a data conversion, I'm fully aware of the violation of strong typing -- I know that and am not concerned. But what I am concerned about is just how the data conversions are done, and in C, etc., I don't see the documentation to tell me. In strong contrast, PL/I was very careful to document how the data conversions would be done.
So, each time I see cast without details of how the data conversions are done, I'm ready to scream.
Am I the only one who cares about the details of the data conversions than the violation of strong typing? Again, to me, the main issue is the data conversion, not the strong typing.
So in C, casting is a language syntax that provides and explicit way to specify type conversion (rather than the implicit conversion that happens in cases like promotion). Most type conversions
Section 6.2.5 contains the defined type conversions, but most of what I'd consider edge cases are "implementation-defined" or have "undefined behavior".
Does any of this bother me? No, not really. No more than it bothers me that there isn't a single, authoritative, and clear definition of what constitutes "strong typing" and which operations are a "violation" of it.
(Linked from Wikipedia as being the C99 standard "effectively available for free"): http://www.open-std.org/jtc1/sc22/WG14/www/docs/n1256.pdf
Gee, that's a long way ahead of what I got out of reading K&R!
In
> So in C, casting is a language syntax
right. So, cast is just syntax but short on semantics, that is, how the conversion is actually done so that we can know the results, that is, if you will, what the heck happens.
In
6.3.1.5 Real floating types
is
> If the value being converted is in the range of values that can be represented but cannot be represented exactly, the result is either the nearest higher or nearest lower representable value, chosen in an implementation-defined manner. If the value being converted is outside the range of values that can be represented, the behavior is undefined.
So, yes, this is an example of where I'd be concerned.
On "strong typing", I always thought that the concept was poorly defined and described but never screamed bloody murder about it if only because it didn't directly affect what my software would do. I used "strong typing" in my post if only because it seems to be what the C community likes to do, and when in Rome do as Romans do. Or, maybe strong typing is better than weak typing or typeless -- okay by me.
Bjarne has said that "this" should have been a reference; making it a pointer was a mistake. But the early design of constructors pointed the language in the wrong direction.
At least you can't assign to "this" any more. You can still assign to "*this", but probably shouldn't.
Not terribly great. C++ references are neither move nor copy-assignable, which greatly limits their usefulness. For example, you can't use a reference as a loop counter. This means that references can never fully replace pointers: pointers are very natural loop counters when manipulating linked data structures.
What C++ needs is a movable, copyable, just non-nullable pointer type.
> Bjarne has said that "this" should have been a reference; making it a pointer was a mistake.
Wholeheartedly agree.
The point of move semantics is to enable use of values instead of pointers. That's even better/safer than using references.
Rust's borrow checker understands invalidation via move, and lots of other changes in variable state which occur along execution paths. That seems the way to go. But it has to be integrated into the language design. As a backwards compatible bolt-on to C++, it just won't work.
(It's been tried. I tried once, about 10 years ago, and gave up. I talked to the designers of Rust, and they tried and gave up. There have been other attempts to get C++ ownership under compile time checking, and the result is either a new incompatible C++ variant or something that leaks like a sieve. Ownership needs to be a fundamental language concept independent of type)
The only satisfactory solution is to enforce, using the type system, the rule that moved objects can't be reused. Unfortunately, C++'s type system can't express this.
I'd agree except that using references leaves no hint at call sites that a reference is being passed. If you're using pointers, the pointer sigil (&) at call sites can serve as a really obvious signpost that something non-trivial is going on.
(This is really unfortunate, IMO, but there's no changing it at this point.)
#include <iostream>
using namespace std;
class myclass
{
public:
int sum(int a, int b)
{
cout << this << endl;
return a + b;
}
};
int main()
{
cout << ((myclass*)0)->sum(10, 11) << endl;
return 0;
}
this will print:
0
21And it's not a null pointer dereference because in C++ it's the function responsible to know the class its belongs to. It means that this code:
((myclass*)0)->sum(10, 11)
should become: sum(0 /*this*/, 10, 11)
and not 0->sum(10, 11)
It can be UB in the C++ specification, but considering the systemv abi, 'this' is just a parameter that will be pushed in the call stack before calling 'sum'. #include <iostream>
using namespace std;
class myclass
{
public:
virtual int sum(int a, int b)
{
return a + b;
}
};
int main()
{
myclass test;
cout << test.sum(1, 2) << endl;
cout << ((myclass*)0)->sum(10, 11) << endl;
return 0;
}
Debugging: (gdb) b main
Breakpoint 1 at 0x4008ae: file test.cpp, line 16.
(gdb) run
Starting program: /tmp/test
Breakpoint 1, main () at test.cpp:16
16 myclass test;
(gdb) ni
17 cout << test.sum(1, 2) << endl;
(gdb) info vtbl test
vtable for 'myclass' @ 0x400a30 (subobject @ 0x7fffffffe410):
[0]: 0x400974 <myclass::sum(int, int)>
Notice that the function address to be called is not know at compile time, the compiler wrote a lookup table, named vtable, to know which function it will call based on the class instance. (gdb) disassemble main
[snip]
0x00000000004008ae <+8>: movq $0x400a30,-0x10(%rbp) ; see the vtable address here
=> 0x00000000004008b6 <+16>: lea -0x10(%rbp),%rax ; ohh, the vtable address is the 'myclass test' instance*
0x00000000004008ba <+20>: mov $0x2,%edx
0x00000000004008bf <+25>: mov $0x1,%esi
0x00000000004008c4 <+30>: mov %rax,%rdi ; pass this to RDI
0x00000000004008c7 <+33>: callq 0x400974 <myclass::sum(int, int)>
* it means that the first (and only one, in this case) field of my myclass is that vtable. (gdb) print test
$1 = {_vptr.myclass = 0x400a30 <vtable for myclass+16>}
(gdb) info register rax
rax 0x7fffffffe410 140737488348176
(gdb) x/x 0x7fffffffe410
0x7fffffffe410: 0x00400a30
(gdb) x/x 0x00400a30
0x400a30 <_ZTV7myclass+16>: 0x00400974
Then, the compiler knows that it must call '[0]: 0x400974 <myclass::sum(int, int)>' function.Compare with:
((myclass*)0)->sum(10, 11)
0x00000000004008e5 <+63>: mov $0x0,%eax ; vtable address is 0 :(
0x00000000004008ea <+68>: mov (%rax),%rax ; the address to RAX is 0, and we cannot access that segment of memory - BOOOM
0x00000000004008ed <+71>: mov (%rax),%rax
0x00000000004008f0 <+74>: mov $0xb,%edx
0x00000000004008f5 <+79>: mov $0xa,%esi
0x00000000004008fa <+84>: mov $0x0,%edi
0x00000000004008ff <+89>: callq *%raxFirst, within myclass::sum, the compiler can assume that "this" will never be null. In your example, you pass "this" to cout; if the iostream code is inlined, and it has a conditional on the passed pointer, for instance to special-case the output of a null pointer, the compiler will omit the comparison and output only the code for the non-null case.
Second, within the main function. The compiler can see you are calling a method with a null "this" pointer. Since this is undefined behavior, it clearly can't happen, and there must be something earlier in the call path that leads to that part of the code never being executed. Therefore, since this is the first thing in the main function, the compiler can assume that main() will never be called, and replace it with an empty function (to satisfy the linker).
If your compiler doesn't do that, it only means that it's not smart enough yet. The behavior can change in later versions of your compiler, or if you use a different compiler.
Sure, undefined behavior can assume anything, but the compiler writer still has to respect the ABI, right?
I was reading the Itanium C++ ABI draft and the only thing I found about it is:
"If a class has a non-virtual destructor, and a deleting destructor is emitted for that class, the deleting destructor must correctly handle the case that the this pointer is NULL. All other destructors, including deleting destructors for classes with a virtual destructor, may assume that the this pointer is not NULL." https://mentorembedded.github.io/cxx-abi/abi.html
However, you can see that I don't have an instance, there is no object so the destructor won't be called. It means that the compiler writer won't probably consider checking 'this' pointer for null - clang/gcc both don't care:
% g++-4.9 -Wall -O3 -std=c++11 test.cpp -o test; ./test
0
21
% clang++ -Wall -O3 -std=c++11 test.cpp -o test; ./test
0
21
0x400886 <main()> push %rbp
0x400887 <main()+1> mov %rsp,%rbp
0x40088a <main()+4> mov $0xb,%edx
0x40088f <main()+9> mov $0xa,%esi
0x400894 <main()+14> mov $0x0,%edi
0x400899 <main()+19> callq 0x400910 <myclass::sum(int, int)>
The 'this' pointer is just a parameter in the register EDI.NOTE: I don't recommend anyone writing code like this. I'm always concerned about UB and I agree with you. But it's still interesting to know how things really are.
test rax, rax
add rax, $offset
cmovz rax, 0
Or keep the damn jump and expect the cpu to profile it correctly.Sorry for sounding angry, but this kind of thing isn't what makes my programs faster, this kind of thing is what makes it harder to write correct programs.
Edit: clang does exactly this at -O1: https://godbolt.org/g/o6gh0M
This is just plain ugly. Why should the normal operation of a program written in a high-level language trigger page faults? But, leaving aesthetic concerns aside, I'm not even sure it works. How do you guarantee that the OS won't give you back the same pages you unmapped when you try to map new pages (say, to grow the heap)?
Yeah. Unfortunately the only really efficient protection mechanisms that modern processors have is virtual memory. C++ programs generally unmap the first few pages for exactly the same reason; to catch nullptr derefs.
> Why should the normal operation of a program written in a high-level language trigger page faults?
NullPointerExceptions are not considered normal operation. They are safety violations that have a controlled, well-defined semantics. BTW page faults happen all the time; the OS transparently handles them and maps in new pages as necessary. The problem you are referring occurs when a page fault happens and the OS knows there is no mapping for those addresses.
> How do you guarantee that the OS won't give you back the same pages you unmapped
Because the mapping is for an address space range (e.g. 0-N), and the OS does not overlap new request with existing mappings unless specified in the request.
Ah, so by “unmap”, you actually something like POSIX's `mprotect()`, rather than `munmap()`?
I'm not really buying this. My definition of “normal operation” is very simple: Everything but FFI calls. Normal operation in a safe language is supposed to be safe.
When the garbage collector needs to run it protects the page and waits until all threads have segfaulted and transferred control to the collector.