0

Given a WinAPI function which returns it's result via a C style string OUT parameter e.g.:

int WINAPI GetWindowTextW(
   _In_   HWND hWnd,
   _Out_  LPTSTR lpString,
   _In_   int nMaxCount
);

Is there a better way of using the function than what I'm doing below?

HWND handle; // Assume this is initialised to contain a real window handle
std::wstring title;
wchar_t buffer[512];
GetWindowTextW(handle, buffer, sizeof(buffer));
title = buffer;

The above code works, but I have the following issues with it:

  1. The buffer size is completely arbitrary since I have no way to know the length of the string that the function might return. This "feels" wrong to me - I have always tried to avoid magic numbers in my code.

  2. If the function returns a string which is larger than the buffer, it will get truncated - this is bad!

  3. Whenever the function returns a string which is smaller than the buffer, I will be wasting memory. This is not as bad as (2), but I'm not thrilled about the idea of setting aside large chunks of memory (e.g. 1024 bytes in my example above) for something that might only need a few bytes in practice.

Are there any other alternatives?

JBentley
  • 6,099
  • 5
  • 37
  • 72
  • 1
    Oh my god.. are you the real Jon Bentley? – Aniket Inge Feb 05 '13 at 19:57
  • 3
    You should be using `_countof`, not `sizeof` - the parameter required is maximum character count, not byte count. Otherwise no, there's not really a better way than how you're using it - although "wasting" a few hundred bytes of stack is not really a major deal, and you can always wrap that code in { } if you want to reclaim the stack immediately. – Jonathan Potter Feb 05 '13 at 20:01
  • You can use `GetWindowTextLength()` to determine the length of the string and allocate an appropriately sized buffer. – Pete Feb 05 '13 at 20:04
  • @Pete Thanks, if you post that as an answer I'll upvote it, since it's a good one for the specific example I gave, if not the general problem. – JBentley Feb 05 '13 at 20:10
  • why not `std::wstring title(buffer)`? – Pavel Radzivilovsky Feb 05 '13 at 20:10
  • or better: `std::string title(narrow(std::wstring(title(buffer))));` for better handling of unicode. (which means, UTF-8. utf8everywhere.org) – Pavel Radzivilovsky Feb 05 '13 at 20:11
  • @Pavel Well, the example was contrived for simplicity. In my real code, `title` already exists elsewhere. – JBentley Feb 05 '13 at 20:40
  • Check out http://stackoverflow.com/questions/584824/guaranteed-lifetime-of-temporary-in-c for an alternate approach. – Mark Ransom Feb 05 '13 at 20:41

2 Answers2

3

Call the function multiple times with temporary buffers of different sizes. Begin with a buffer of, say, 8. Double the buffer size and call it again. Repeat until it returns the same count as the last time. Then you can allocate the exact size buffer and copy what you've got there. There are a number of Win32 functions with similar behavior.

You may use GetWindowTextLength(), but it probably won't help much if there are race conditions (you may end up with a truncated text because of them).

Alexey Frunze
  • 61,140
  • 12
  • 83
  • 180
  • 1
    Interesting hack, thanks. I'll consider this if I can't find an equivalent to GetWindowTextLength() for other WinAPI functions (as per Pete's suggestion in the comments to my question). – JBentley Feb 05 '13 at 20:11
  • 2
    Alexey can go ahead and take the checkmark for this... But I would point out that there are usually ways of doing something similar with many of the Windows API functions of this type. Many return the number of bytes or characters in the data and you can pass in a null buffer first to get the actual length and then use that to create your buffer. Very kludgy, I think, but that's the WinAPI for you. – Pete Feb 05 '13 at 20:27
2

There are a few different patterns depending on which Windows API function you're using. With some, you can query first, sometimes by calling another function (e.g., GetWindowTextLengthW) but usually by passing NULL for the buffer. After you query, you allocate the size and call again to get the actual string data.

Even with query-allocate-query, you sometimes need to iterate as there could be a race condition. For example, consider what happens if the window title changes between the GetWindowTextLengthW and the GetWindowTextW calls.

You can also avoid an extra copy by using the string itself rather than a second buffer.

std::wstring GetWindowTitle(HWND hwnd) {
    std::wstring title(16, L'X');
    int cch;
    do {
      title.resize(2 * title.size());
      cch = GetWindowTextW(hwnd, &title[0], title.size());
    } while (cch + 1 == title.size());
    title.resize(cch);
    return title;
}

While awkward, this isn't really a fault of the Windows API design. The API is designed to be a C interface, not C++. Since C is not object-oriented, it's pretty limited when it comes to handling strings. For C++ code, you can wrap this kind of bookkeeping as I've done in this example.

Adrian McCarthy
  • 45,555
  • 16
  • 123
  • 175
  • Your `GetWindowTitle()` example had several bugs. If `GetWindowTextLength()` returns `0`, the code will loop endlessly. If it returns `>0`, the code will never loop more than 1 iteration because `cch` can never be `>= title.size()`. `GetWindowText()` returns the number of characters copied *not including* the null terminator, but the second parameter specifies the maximum characters that can be copied *including* the null terminator, so `cch` will always be `< title.size()` and truncate the result, except when both functions return 0. I corrected it. – Remy Lebeau Feb 05 '13 at 23:09
  • @RemyLebeau: Thanks. I got interrupted in the middle of preparing the answer and inadvertently submitted before it was ready. When I get a chance, I'll take another stab at it since it's still not perfect. – Adrian McCarthy Feb 06 '13 at 00:04
  • You were not allocating the initial `wstring` correctly, not doing error handling on `GetWindowText()`, and not taking the null terminator into account correctly. The size passed in to `GetWindowText()` includes room for a null terminator, but the return value does not include the null terminator. `size()` does not include the null terminator that is at the end of the `wstring` memory block. – Remy Lebeau Feb 06 '13 at 02:36
  • @RemyLebeau: Both of your edits have introduced a buffer overrun. My last version was tested with an empty title, a short title, and a very long title and returned a correctly-sized string in all cases with no buffer overrun. – Adrian McCarthy Feb 06 '13 at 16:58
  • I tested every edit before posting it here, and was not having any overrun issues. But whatever. Personally, I would probably have switched to a `std::vector` instead and then copied it into a `wstring` at the end of the function. That would avoid any issues with `wstring::size()` and null terminators. – Remy Lebeau Feb 06 '13 at 21:21
  • With pre-C++11 strings, the size() may be the actual size of the buffer. So when you told GetWindowText that the buffer was `title.size() + 1`, you're telling the function that it can write a `\0` beyond the end of the buffer. In C++11, the string implementations have been further constrained to essentially require the extra character in the internal buffer. – Adrian McCarthy Feb 06 '13 at 23:14
  • In every pre-C++11 STL implementation I have worked with, `size()` and `length()` are the same value - the number of characters not including the null terminator, but room for the null terminator is allocated, hense the `+1`. All the more reason to switch to a `std:::vector` until the final result is gotten. No need to rely on private implementation details of how `wstring` works internally. – Remy Lebeau Feb 07 '13 at 00:07
  • One of the main points of my answer was to show that you don't need to make an extra copy (which the original question and the accepted answer were doing). Loading into a vector and then assigning to a string makes an extra copy. Not a big deal, but it was what I was trying to avoid. – Adrian McCarthy Feb 07 '13 at 00:53
  • Pre-sizing the `wstring` to 16 characters and then immediately resizing it to 32 characters before the first call to `GetWindowText()` is not ideal, either. And resizing a `wstring` to shrink it at the end does not guarantee a copy is not still made, either. But oh well. This diiscussion has gone on long enough. – Remy Lebeau Feb 07 '13 at 02:53
  • 1
    @Remy Thanks for all the useful comments, from both of you. I'd be grateful if you could clarify what you mean about switching to `std::vector`, and how this would alleviate the problem you described? – JBentley Mar 24 '13 at 05:14