Hacker News new | past | comments | ask | show | jobs | submit login
A static_cast is not always just a pointer adjustment (microsoft.com)
45 points by Tomte on April 2, 2016 | hide | past | favorite | 39 comments



Multiple inheritance in its many forms (including interfaces as found in Java/C#/Golang) always makes casts interesting, even sometimes when methods aren't involved.

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...


I tried, but I never could understand cast in C or anywhere else. On the other hand, for decades I've seen, seen documentation for, and used several data type conversions among common elementary data types -- binary, 2's complement integers, decimal, base 2 floating point, base 16 floating point, precision differences, character strings, etc.

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.


Cast (according to the C99 standard, section 6.3, linked at bottom): A construction formed by preceding an expression by a parenthesized type name, converting the value of the expression to the named type.

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


Thanks for the reference.

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.


Reminds me of point 3 in http://www.sebastiansylvan.com/post/language-design-deal-bre...: “You have to make sure that nullable pointers cannot be dereferenced. (...) Having every few statements potentially trigger a runtime crash is an expensive price to pay... for nothing!” [emphasis in the original] Once you have proper non-nullable pointer types, `static_cast` is a single pointer bump once again.


This is why C++ code should use references more and pointers less. How's that working out in the era of move semantics?

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.


> This is why C++ code should use references more and pointers less. How's that working out in the era of move semantics?

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.


> This is why C++ code should use references more and pointers less. How's that working out in the era of move semantics?

The point of move semantics is to enable use of values instead of pointers. That's even better/safer than using references.


Unfortunately, move semantics in C++ is broken, and will remain so until the type system finally understands that `std::move` is supposed to invalidate the original object. But, this being C++, I won't hold my breath.


Trying to fix this with the type system doesn't really work. Tracking the changing attributes along a path of control flow with the type system requires an insanely complicated type system.

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)


I didn't mean to suggest this can be done with C++'s existing type system.


Have you seen the Core Gudelines and the GSL? Not as powerful as Rust, but very interesting.


The C++ Core Guidelines aren't always mechanically enforceable. Experience shows that unenforceable rules will be broken. After all, what's undefined behavior, if not an unenforceable “don't do this” rule?


Also, they don't deal with concurrency currently, either.


I thought moving will leave the original in some valid state. For example moving a vector may leave the original object as an empty vector. What's so broken about that?


Sometimes there is no valid state for a `std::move`d object, e.g. a `std::unique_ptr`.


Wouldn't just be nullptr?


The very concept of a nullable pointer is problematic, because it means that every dereference operation is potentially unsafe.

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.


> This is why C++ code should use references more and pointers less.

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.)


No, he has said that "this" is a pointer because it was added to the language before references were added. This is covered in his FAQ.


It's possible to have 'this' == NULL. Consider the code:

  #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 21

And 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'.


The problem with undefined behavior, is that the compiler is allowed to assume that it will not happen.

First, 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.


I didn't understand why my point was downvoted, anybody who likes to know how things work on backstage or likes to hack stuff would find it interesting.

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.


This is still undefined behavior. Sometimes undefined behavior does what you want, but you can't count on it.


What happens if sum is a virtual function? Or does it not matter? I am not too familiar with c++ casting magic.


Most likely you'll get a segfault when it tries to read from page 0 to get to the class' vtable (which is where the references to the virtual functions live).


Yes, you'll get a segmentation fault, consider this code, compiled with debug information:

  #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  *%rax


static_cast is used mainly to "reuse" existing code base


I use it mostly to switch between numerical types like double, float, int, uint32_t, char, size_t.


Jesus Fucking Christ, now we're exploiting undefined behaviour to remove one compare instruction? You don't even need the jump, just use a conditional move. Just..

    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


A small nitpick, your code is incorrect because the add operation will set ZF.


That you need to invoke undefined behavior to remove the compare instruction is only the symptom. The actual disease is nullable pointers themselves, which create the need for a compare instruction at every pointer dereference in the first place.


No. Java has nullable pointers and almost all JVMs implement them with a pointer to address 0 and unmap the first couple of pages of the address space. That will cause a fault, leading to a signal, which is then handled to materialize and throw the exception. No compare. Just naked loads/stores with fixed offsets. The disease is multiple (implementation) inheritance.


> No. Java has nullable pointers and almost all JVMs implement them with a pointer to address 0 and unmap the first couple of pages of the address space.

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)?


> This is just plain ugly.

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.


> NullPointerExceptions are not considered normal operation. They are safety violations that have a controlled, well-defined semantics.

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.


> 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()`?


Sorry, yes. You can do this just through segment declarations in both ELF and MachO binary formats, to prevent anything getting accidentally mapped there before startup.


Garbage collectors (and various other parts of the JVM) use similar tricks to coordinate threads without using memory barriers. The compiler just inserts TEST instructions to some fixed memory address in strategic places in the code.

When the garbage collector needs to run it protects the page and waits until all threads have segfaulted and transferred control to the collector.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: