Работа с функциями WinAPI, которые используют строки в стиле C в качестве выходных параметров.

Учитывая функцию WinAPI, которая возвращает результат через параметр OUT строки в стиле C, например:

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

Есть ли лучший способ использования этой функции, чем то, что я делаю ниже?

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;

Приведенный выше код работает, но у меня есть следующие проблемы с ним:

  1. Размер буфера совершенно произвольный, так как я не могу узнать длину строки, которую может вернуть функция. Мне это "кажется" неправильным - я всегда старался избегать магических чисел в своем коде.

  2. Если функция возвращает строку, превышающую размер буфера, она будет усечена — это плохо!

  3. Всякий раз, когда функция возвращает строку, которая меньше буфера, я буду тратить память. Это не так плохо, как (2), но я не в восторге от идеи выделения больших кусков памяти (например, 1024 байта в моем примере выше) для чего-то, что на практике может потребовать всего несколько байтов.

Есть ли другие альтернативы?


person JBentley    schedule 05.02.2013    source источник
comment
Боже мой... ты настоящий Джон Бентли?   -  person Aniket Inge    schedule 05.02.2013
comment
Вы должны использовать _countof, а не sizeof - требуемый параметр - это максимальное количество символов, а не количество байтов. В противном случае нет, на самом деле нет лучшего способа, чем то, как вы его используете, хотя потеря нескольких сотен байтов стека на самом деле не является серьезной проблемой, и вы всегда можете обернуть этот код в {}, если хотите немедленно восстановить стек .   -  person Jonathan Potter    schedule 06.02.2013
comment
Вы можете использовать GetWindowTextLength(), чтобы определить длину строки и выделить буфер соответствующего размера.   -  person Pete    schedule 06.02.2013
comment
@Pete Спасибо, если вы опубликуете это как ответ, я проголосую за него, так как он хорош для конкретного примера, который я привел, если не для общей проблемы.   -  person JBentley    schedule 06.02.2013
comment
почему не std::wstring title(buffer)?   -  person Pavel Radzivilovsky    schedule 06.02.2013
comment
или лучше: std::string title(narrow(std::wstring(title(buffer)))); для лучшей обработки юникода. (что означает UTF-8. utf8everywhere.org)   -  person Pavel Radzivilovsky    schedule 06.02.2013
comment
@Pavel Ну, пример был придуман для простоты. В моем реальном коде title уже существует в другом месте.   -  person JBentley    schedule 06.02.2013
comment
Проверьте stackoverflow.com/questions/584824/ для альтернативного подхода.   -  person Mark Ransom    schedule 06.02.2013


Ответы (2)


Вызовите функцию несколько раз с временными буферами разного размера. Начните с буфера, скажем, 8. Удвойте размер буфера и вызовите его снова. Повторяйте, пока он не вернет тот же счетчик, что и в прошлый раз. Затем вы можете выделить буфер точного размера и скопировать то, что у вас есть. Существует ряд функций Win32 с аналогичным поведением.

Вы можете использовать GetWindowTextLength(), но это, вероятно, не сильно поможет, если есть условия гонки (из-за них вы можете получить усеченный текст).

person Alexey Frunze    schedule 05.02.2013
comment
Интересный лайфхак, спасибо. Я рассмотрю это, если не смогу найти эквивалент GetWindowTextLength() для других функций WinAPI (согласно предложению Пита в комментариях к моему вопросу). - person JBentley; 06.02.2013
comment
Алексей может пойти дальше и поставить галочку за это... Но я хотел бы отметить, что обычно есть способы сделать что-то подобное со многими функциями Windows API этого типа. Многие возвращают количество байтов или символов в данных, и вы можете сначала передать нулевой буфер, чтобы получить фактическую длину, а затем использовать ее для создания своего буфера. Я думаю, что это очень глупо, но это WinAPI для вас. - person Pete; 06.02.2013

Существует несколько различных шаблонов в зависимости от того, какую функцию Windows API вы используете. В некоторых случаях вы можете сначала выполнить запрос, иногда вызывая другую функцию (например, GetWindowTextLengthW), но обычно передавая NULL для буфера. После запроса вы выделяете размер и вызываете снова, чтобы получить фактические строковые данные.

Даже с запросом-распределением-запросом вам иногда нужно повторять, так как может возникнуть состояние гонки. Например, рассмотрим, что произойдет, если заголовок окна изменится между вызовами GetWindowTextLengthW и GetWindowTextW.

Вы также можете избежать дополнительной копии, используя саму строку, а не второй буфер.

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;
}

Хотя это неудобно, на самом деле это не ошибка дизайна Windows API. API разработан как интерфейс C, а не C++. Поскольку C не является объектно-ориентированным, он довольно ограничен, когда дело доходит до обработки строк. Для кода C++ вы можете обернуть этот вид учета, как я сделал в этом примере.

person Adrian McCarthy    schedule 05.02.2013
comment
В вашем примере GetWindowTitle() было несколько ошибок. Если GetWindowTextLength() возвращает 0, код будет бесконечно повторяться. Если он возвращает >0, код никогда не будет зацикливаться более чем на 1 итерацию, потому что cch никогда не может быть >= title.size(). GetWindowText() возвращает количество скопированных символов, не включая нулевой терминатор, но второй параметр указывает максимальное количество символов, которые можно скопировать, включая нулевой терминатор, поэтому cch всегда будет < title.size() и обрезать результат, за исключением случаев, когда обе функции возвращают 0. Я исправил это. - person Remy Lebeau; 06.02.2013
comment
@RemyLebeau: Спасибо. Меня прервали в середине подготовки ответа, и я случайно отправил его до того, как он был готов. Когда у меня будет шанс, я попробую еще раз, так как он все еще не идеален. - person Adrian McCarthy; 06.02.2013
comment
Вы неправильно выделяли начальный wstring, не выполняли обработку ошибок в GetWindowText() и неправильно учитывали нулевой терминатор. Размер, переданный в GetWindowText(), включает место для нулевого ограничителя, но возвращаемое значение не включает нулевой ограничитель. size() не включает нулевой терминатор, который находится в конце блока памяти wstring. - person Remy Lebeau; 06.02.2013
comment
@RemyLebeau: Оба ваших редактирования привели к переполнению буфера. Моя последняя версия была протестирована с пустым заголовком, коротким заголовком и очень длинным заголовком и во всех случаях возвращала строку правильного размера без переполнения буфера. - person Adrian McCarthy; 06.02.2013
comment
Я проверял каждое редактирование, прежде чем публиковать его здесь, и у меня не было проблем с переполнением. Но что угодно. Лично я, вероятно, вместо этого переключился бы на std::vector, а затем скопировал бы его в wstring в конце функции. Это позволит избежать проблем с терминаторами wstring::size() и null. - person Remy Lebeau; 07.02.2013
comment
Для строк до C++11 size() может быть фактическим размером буфера. Поэтому, когда вы сообщаете GetWindowText, что буфер равен title.size() + 1, вы сообщаете функции, что она может записать \0 за конец буфера. В C++11 строковые реализации были дополнительно ограничены, чтобы по существу требовать дополнительный символ во внутреннем буфере. - person Adrian McCarthy; 07.02.2013
comment
В каждой реализации STL до C++11, с которой я работал, size() и length() имеют одно и то же значение - количество символов, не включая нулевой терминатор, но выделяется место для нулевого терминатора, отсюда и +1. Тем больше причин переключиться на std:::vector, пока не будет получен окончательный результат. Не нужно полагаться на частные детали реализации того, как wstring работает внутри. - person Remy Lebeau; 07.02.2013
comment
Одним из основных моментов моего ответа было показать, что вам не нужно делать дополнительную копию (что делали исходный вопрос и принятый ответ). Загрузка в вектор и последующее присвоение строке создает дополнительную копию. Ничего страшного, но это было то, чего я пытался избежать. - person Adrian McCarthy; 07.02.2013
comment
Предварительное изменение размера wstring до 16 символов, а затем немедленное изменение его размера до 32 символов перед первым вызовом GetWindowText() также не является идеальным. И изменение размера wstring, чтобы уменьшить его в конце, также не гарантирует, что копия не будет сделана. Но да ладно. Эта дискуссия продолжалась достаточно долго. - person Remy Lebeau; 07.02.2013
comment
@Remy Спасибо за все полезные комментарии от вас обоих. Буду признателен, если вы поясните, что вы имеете в виду под переходом на std::vector и как это облегчит описанную вами проблему? - person JBentley; 24.03.2013