3

Beginner question.

In Win32API, the second parameter of ::GetClientRect is LPRECT, but It receives both CRect and CRect*.

I tried to see how LPRECT is defined, but it seems just an ordinary pointer to struct:

typedef struct tagRECT
{
    LONG    left;
    LONG    top;
    LONG    right;
    LONG    bottom;
} RECT, *PRECT, NEAR *NPRECT, FAR *LPRECT;

And CRect inherits tagRECT:

class CRect :public tagRECT {...}

Using MFC, CWnd::GetClientRect works the same way:

CRect rect;
GetClientRect(&rect); // works
GetClientRect(rect);  // works too.

How is this possible? Please let me know if I am missing some basic concepts of C++.

starriet
  • 2,565
  • 22
  • 23
  • 10
    `CRect` has an [`operator LPRECT()`](https://learn.microsoft.com/en-us/cpp/atl-mfc-shared/reference/crect-class?view=msvc-170#operator_lprect) conversion operator that services this to a single member. – WhozCraig Sep 14 '22 at 02:16
  • For future reference) a simple [example](https://stackoverflow.com/a/60355982/10027592). – starriet Oct 05 '22 at 13:54

1 Answers1

7

During overload resolution1 the compiler builds a set of candidate functions that match the arguments. If none of the functions' signatures exactly match the arguments the compiler will then try to apply implicit conversions, at most one per argument.

That's what makes both calls possible, though the implicit conversion is different in either case:

GetClientRect(&rect);

In this function call expression, &rect has type CRect*, with CRect publicly deriving from RECT, so an implicit pointer conversion from CRect* to RECT* (aka LPRECT) is done.

GetClientRect(rect);

Here, on the other hand, rect has type CRect. C++ doesn't provide any built-in conversions from CRect to RECT*, so user-defined conversions are considered. Specifically, there's a conversion operator CRect::operator LPRECT that produces a RECT* pointer given a CRect object.

It is of crucial importance to appreciate, that both function call expressions do different things. In the second case the compiler literally injects a call to operator LPRECT() prior to calling GetClientRect(). There's nothing in the language that mandates that the conversion operator must behave any given way, and it's down to library authors to provide a sane implementation.

In this case, operator LPRECT() behaves as one would expect, and both function calls have the same observable behavior. Now that you have two ways to do the same thing, guidance is required to make a judicious choice:

While there are no strict rules on which of the options to use, it's down to opinion. I would recommend GetClientRect(&rect) for a few reasons:

  • Easier to understand for readers of the code
    • Pointer arguments are frequently used as [out] parameters where implementations write their results
    • Less ambiguous: GetClientRect(rect) looks like pass-by-value, or maybe the object is bound to a reference. In reality, neither one it is, and the (invisible) conversion operator is called instead
  • No more expensive than the version that uses the conversion operator

1 Overload resolution is a core language feature. The set of rules driving this part of the language, however, make it anything but basic. It's something you'll eventually have to learn, but it's a very steep learning curve. The cppreference.com link gives you a glimpse at the complexity involved.

IInspectable
  • 46,945
  • 8
  • 85
  • 181
  • So, in *both* calls, none of the functions' signatures exactly match the arguments, so the compiler tries 'implicit conversion', but in different ways, right? Another independent question: Why "at most *one* per argument"? Is there any problem if the compiler tries implicit conversions *multiple* times until it finds the right match to the function signature? Thank you for this great answer! – starriet Sep 15 '22 at 14:39
  • 2
    @starriet That's correct, neither `&rect` nor `rect` are of type `RECT*`, so in either case a conversion is required. And yes, those conversion are different (but probably compile to the exact same machine code with optimizations). The compiler will try no more than one conversion. That's just part of the language specification. I don't know the reason behind this rule, though I guess that otherwise the overload sets would grow exponentially. Not the kind of thing you'd want when writing a compiler in the 1980's. Plus, it'd introduce ambiguities that would need to be dealt with. – IInspectable Sep 15 '22 at 15:51