Const pointers: Logical consequences


Consider this follow-up question to the question from last time:

When I call the PropertySheet function, can I assume that the phpage field of the PROPSHEETHEADER structure will not be modified?

If we take a look at the declaration of the PropertySheet function, we see that it reads like this:

typedef const PROPSHEETHEADERA *LPCPROPSHEETHEADERA;
typedef const PROPSHEETHEADERW *LPCPROPSHEETHEADERW;

WINCOMMCTRLAPI INT_PTR WINAPI PropertySheetA(LPCPROPSHEETHEADERA);
WINCOMMCTRLAPI INT_PTR WINAPI PropertySheetW(LPCPROPSHEETHEADERW);

Go past all the function declaration specification goo and look at the parameter list. It's a const pointer to a PROPSHEETHEADER structure (either ANSI or Unicode, depending on which flavor of the PropertySheet function you're calling).

One of the rules for const pointers is that you can read from them but you cannot write to them. Consequently, the PropertySheet function is not allowed to modify the PROPSHEETHEADER structure. Assuming your code doesn't modify the PROPSHEETHEADER yourself, any value on exit from the function will be the same as the value it had on entry.

Comments (19)
  1. Dave Harris says:

    In ANSI C++, deleting an object is considered not to change it. So for example:

       void kill( const int *p ) {

           delete p;

       }

    will compile. It’s a bizarre rule that the standard added – it wasn’t true for ARM C++.

  2. Nish says:

    Dave

    I don’t see anything bizarre about that. Surely we need to be able to delete pointers to const objects, else for any non-trivial app, we’d soon be running out of memory.

  3. dave says:

    There probably wouldn’t be so many questions if Windows didn’t use such silly typedefs.

    Given this prototype, is the argument const?

     PropertySheetW(LPCPROPSHEETHEADERW);

    The approved answer is “Well, of course, there’s a C in the type name”.

    The prototype is a whole lot easier understand if it’s not willfully obfuscated:

     PropertySheetW(const PROPSHEETHEADERW *);

    This is readily understandable by any C programmer (well, except those who haven’t qute got round to understanding new-fangled concepts like ‘const’).

    Tell me again why these LPCBANANA typedefs are a good idea?  

    [They were much more useful in 16-bit Windows and now it’s just tradition. -Raymond]
  4. Nish says:

    Ray,

    Why were they useful in 16-bit Windows?

    And sorry for commenting 3 times in this blog entry – I hope that’s alright (once in a while).

  5. KiwiBlue says:

    Nish,

    In Win16 days, you had 16 and 32-bit pointers. The "LP" stands for "Long Pointer". The typedefs were used to ensure that programs compiled in small and medium memory models were using right pointer types when calling API functions.

    BTW: probably you should avoid "Ray"-ing here :)

  6. Adam says:

    “Assuming your code doesn’t modify the PROPSHEETHEADER yourself, any value on exit from the function will be the same as the value it had on entry.”

    Not entirely true. If the PROPSHEETHEADER contains pointers to non-const other data (such as phpage), that data can be written to, even though the pointer itself can’t be.

    In fact, AFAICT, “phpage” appears to be the only non-const pointer member of a PROPSHEETHEADER. So, while “phpage” can’t be made to point anywhere else, the data it points to *can* be modified. (But for any other member, your response would have been correct)

    [Ah, perhaps I misunderstood the original question then. I thought the question was “If I memcmp the PROPSHEETHEADER before and after, will there be any difference?” The phpage member will be unchanged. What it points to, however, is another matter. -Raymond]
  7. Centaur says:

    Unfortunately, not all Win32 API functions declare constness of their parameters accurately. Here’s just one example:

    BOOL DrawDibDraw(

     HDRAWDIB hdd, HDC hdc,                  

     int xDst, int yDst, int dxDst, int dyDst,                

     LPBITMAPINFOHEADER lpbi,

     LPVOID lpBits,

     int xSrc, int ySrc, int dxSrc, int dySrc,

     UINT wFlags

    );

    Surely drawing a DIB is not supposed to change it? Why, then, aren’t lpbi and lpBits declared as LPCBITMAPINFOHEADER and LPCVOID, respectively? There isn’t even such a thing as a LPCBITMAPINFOHEADER.

    You might say it’s tradition, or backwards compatibility. But, didn’t all handle types use to all be the same type? Now, with STRICT defined, they are distinct types. Similarly, it should be possible to declare all constant pointers as such in STRICT mode. After all, it’s not like “const” means anything to the binary layout of parameters.

  8. Nish says:

    >>
    typedef const PROPSHEETHEADERA *LPCPROPSHEETHEADERA;
    typedef const PROPSHEETHEADERW *LPCPROPSHEETHEADERW;

    It’s a const pointer to a PROPSHEETHEADER structure (either ANSI or Unicode, depending on which flavor of the PropertySheet function you’re calling).
    <<

    Hey Ray,

    Isn’t that a pointer to a const PROPSHEETHEADER? The pointer itself is not const.

    [Absolutely correct. Sloppy writing on my part. -Raymond]
  9. Adrian says:

    "One of the rules for const pointers is that you can read from them but you cannot write to them."

    Since we’re being pedantic, you *can* write to a pointer to const.  You cannot write *through* such a pointer to the data structure(s) it references.

  10. Adrian’s right, and it’s a great point.

    If you see a function taking a pointer-to-const something, you can’t assume that the structure won’t be modified.  The const does not guarantee it, because that function may have a identical non-const pointer up its sleeve somewhere.

    Or, it can simply cast away the const.  Buh-bye!

    Or, maybe another thread has a non-const pointer to it, and updated in between.

    It’s more of a strong hint that the compiler can help enforce, than a guarantee.

    The only that guarantee is… the documentation.  We may be can able to say "PropertySheet" will not modify that structure, but we can’t extrapolate to all functions that take const pointers.

    Thus, it’s a good question to ask: even though I see a const parameter, does function X respect it?

  11. I find these things easier to understand when written and read from right to left.

    // var is a pointer to a constant T

    T const * var;

    // var is a constant pointer to a T

    T * const var;

    // var is a constant pointer to a constant T

    T const * const var;

    I do agree that the LPCFOOBAR typedefs obfuscate this somewhat.

    PMP

  12. KJK::Hyperion says:

    … the notorious (documented) exception being CreateFileW when the path is of the "\?<device><path>" variety, where the path is going to get converted in-place to "??<device><path>" and passed to the kernel (and then converted back)

  13. Mihai says:

    <<Or, it can simply cast away the const.  Buh-bye!>>

    The const as a promise.

    If I declare this to be a const, I promise you that I am not going to change it.

    When I cast, I am telling the compiler "you shut up, I know better!"

    It is not the compiler’s fault, it is mine, because I am breaking the contract.

  14. Mike says:

    The "const" keyword is clearly something of a random hit-or-miss afterthought in Win32 (and all its half-a-million-or-so COM interface functions now part of a basic Win32-anno-2001 programming interface). Things that clearly should be const are not, and forces casts and bad design decisions to be propagated into otherwise correct program and library code.

    Sure, it can be of interest explaining to the ones not knowing the C language that "const means const", but of much greater interest to at least me is the glaring errors of MS interface designers where they didn’t even bother to try to be const correct – and if that’s a product of ignorance it’s just proof they shouldn’t have been allowed to be interface designers in the first place.

    Just my 0.02.

  15. Norman Diamond says:

    Friday, September 08, 2006 11:31 AM by Adrian

    > "One of the rules for const pointers is that

    > you can read from them but you cannot write

    > to them."

    >

    > Since we’re being pedantic, you *can* write

    > to a pointer to const.  You cannot write

    > *through* such a pointer to the data

    > structure(s) it references.

    As a language rule that is partly true.  An additional rule is that if the actual object really is defined as const then no you can’t write to the actual object even if you have created a pointer-to-nonconst that points to the object.  (Of course the "can’t" might not be enforceable at compile time, and the result is undefined behaviour.)

    Saturday, September 09, 2006 1:02 AM by KJK::Hyperion

    > … the notorious (documented) exception

    > being CreateFileW

    I remembered that the API rule is different from the language rule in that way, but didn’t remember which function.  Now I’m even more confused.  Now I’m reading

    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/createfile.asp

    I do not see documentation of the notorious exception that you mention.  Are you sure that the notorious exception is CreateFile and not some other function?  I had a vague recollection that it involved a parsing function of some kind, writing a _T(‘’) into an element accessed through an LPCTSTR.

    Saturday, September 09, 2006 7:45 AM by Mike

    > Sure, it can be of interest explaining to the

    > ones not knowing the C language that "const

    > means const",

    But it doesn’t.  The distinction between declarations and definitions is crucial.  Even Visual Studio 2005 understands that distinction more than half the time.

  16. GregM says:

    Norman, I think you’re thinking of CreateProcessW.

  17. Dave Harris says:

    Nish: "I don’t see anything bizarre about that."

    It’s bizarre because "does not change" suggests that the value after the function will be the same as it was before the function. For example, in:

       int *p = new int(42);

       kill( p );

       assert( *p == 42 );

    the assert should succeed (there being no aliases or const casts). Indeed, that’s exactly the intuition that Raymond is relying on in the root article. It isn’t true for C++.

    Nish: "Surely we need to be able to delete pointers to const objects, else for any non-trivial app, we’d soon be running out of memory."

    Why? It’s never necessary, because there isn’t really a const heap. The new-expression returns a pointer to a non-const object, and you can use that to delete the object. If you can’t get hold of the original pointer, you can use a const_cast – which is being explicit about the lack of const-correctness in delete.

  18. Norman Diamond says:

    Tuesday, September 12, 2006 11:42 AM by Dave Harris

    > It’s bizarre because "does not change"

    > suggests that the value after the function

    > will be the same as it was before the

    > function.

    If kill(p) means something like free(p) (though of course using C++’s delete keyword instead of the free() function inherited from C) then I think it doesn’t suggest that *p can still be evaluated afterwards.  But if the function had a more innocuous name and if the prototype declaration said const int * then I think it would be as suggestive as you say.

    > The new-expression returns a pointer to a

    > non-const object, and you can use that to

    > delete the object. If you can’t get hold

    > of the original pointer, you can use a

    > const_cast

    Um, who would use a const_cast for that?  Would the const_cast operator appear in some function?  Some caller would call this function with a pointer to const of some sort, and would expect that the pointer could still be dereferenced afterwards?  Are you saying that such a bizarre suggestive function is the right way to go?

  19. Dave Harris says:

    Norman Diamond:

    > But if the function had a more innocuous name

    We’re talking about what semantics can be deduced from the type system. Renaming the function doesn’t affect that.

    > Um, who would use a const_cast for that?  

    Normally you wouldn’t need to, because you’d keep the non-const pointer around.

    > Some caller would call this function with

    > a pointer to const of some sort, and

    > would expect that the pointer could still

    > be dereferenced afterwards?

    That’s the problem, and it exists whether the function has a const_cast or not.

    I’m saying if the pointer can’t be dereferenced afterwards then the function shouldn’t take a const pointer argument. And this should also apply to delete expressions.

    > Are you saying that such a bizarre

    > suggestive function is the right way to go?

    No. Quite the reverse. Currently this is legal but bad:

      void func1( const int *p ) {

          delete p;

      }

    I think this is slightly better:

      void func2( const int *p ) {

          delete const_cast<int *>(p);

      }

    because the function body is at least explicit. Better still is:

      void func3( int *p ) {

          delete p;

      }

    even if this sometimes means the caller needs a const_cast before calling it.

Comments are closed.