Руководство по переносу: Spy++

Этот пример переноса позволяет понять, на что похож типичный проект переноса, какие типы проблем могут возникать, а также содержит ряд общих советов и рекомендаций по решению проблем переноса. Это не является полным руководством по переносу, так как особенности переноса проекта существенно зависят от конкретного кода.

Spy++

Spy++ является широко используемым инструментом диагностики графического интерфейса пользователя для рабочего стола Windows, который предоставляет всевозможные сведения об элементах пользовательского интерфейса на рабочем столе Windows. Он отображает полную иерархию окон и предоставляет доступ к метаданным о всех окнах и элементах управления. Данное приложение поставлялось вместе с Visual Studio в течение нескольких лет. Мы нашли старую версию этого приложения, которое в последний раз было скомпилировано в Visua C++ 6.0, и перенесли его в Visual Studio 2015. Интерфейс Visual Studio 2017 или Visual Studio 2019 должен быть почти идентичным.

Мы считаем данный случай типичным для переноса классических приложений Windows, использующих MFC и API-интерфейс Win32, особенно для старых проектов, которые не обновлялись с каждым выпуском Visual C++ после Visual C++ 6.0.

Шаг 1. Преобразование файла проекта.

Файл проекта, два старых файла DSW из Visual C++ 6.0, были преобразованы без проблем, которые бы требовали дополнительного внимания. Один проект — это приложение Spy++. Другой проект — SpyHk, написанный на языке C, представляющий собой вспомогательную библиотеку DLL. Более сложные проекты, возможно, не удастся обновить так же легко, как это описано здесь.

После обновления двух проектов наше решение выглядело следующим образом:

Screenshot of the Spy plus plus Solution.

У нас есть два проекта — один с большим количеством файлов C++ и другой, представляющий собой библиотеку DLL, которая написана на языке C.

Шаг 2. Проблемы с файлами заголовков

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

Одним из файлов, которые не удалось найти в Spy++, был файл verstamp.h. Благодаря поиску в Интернете мы решили, что это произошло из-за DAO SDK, устаревшей технологии данных. Мы хотели узнать, какие символы использовались из этого файла заголовка, чтобы выяснить, что этот файл действительно нужен или что эти символы определены в другом месте, поэтому мы закомментировали объявление файла заголовка и повторно выполнили компиляцию. Оказалось, что имеется лишь один символ, который нужен, а именно VER_FILEFLAGSMASK.

1>C:\Program Files (x86)\Windows Kits\8.1\Include\shared\common.ver(212): error RC2104: undefined keyword or key name: VER_FILEFLAGSMASK

Самым простым способом поиска символа в доступных включаемых файлах является применение операции поиска в файлах (CTRL+SHIFT+F) и указание каталогов включаемых файлов Visual C++. Мы нашли данный символ в файле ntverp.h. Мы заменили включаемый файл verstamp.h на файл ntverp.h, и эта ошибка исчезла.

Шаг 3. Параметр OutputFile компоновщика

Иногда старые проекты содержат файлы, расположенные в нестандартных папках, что может вызывать проблемы после обновления. В этом случае нужно добавить $(SolutionDir) в путь поиска включаемых файлов в свойствах проекта, чтобы убедиться, что Visual Studio может найти некоторые файлы заголовков в этих расположениях, а не в одной из папок проекта.

MSBuild сообщает, что свойство Link.OutputFile не совпадает со значениями TargetPath и TargetName, выдавая MSB8012.

warning MSB8012: TargetPath(...\spyxx\spyxxhk\.\..\Debug\SpyxxHk.dll) does not match the Linker's OutputFile property value (...\spyxx\Debug\SpyHk55.dll). This may cause your project to build incorrectly. To correct this, please make sure that $(OutDir), $(TargetName) and $(TargetExt) property values match the value specified in %(Link.OutputFile).warning MSB8012: TargetName(SpyxxHk) does not match the Linker's OutputFile property value (SpyHk55). This may cause your project to build incorrectly. To correct this, please make sure that $(OutDir), $(TargetName) and $(TargetExt) property values match the value specified in %(Link.OutputFile).

Link.OutputFile — это выходной файл сборки (например, EXE, DLL). Обычно он создается на основе $(TargetDir)$(TargetName)$(TargetExt), что задает путь, имя файла и расширение. Это распространенная ошибка при миграции проектов из старого инструмента сборки Visual C++ (vcbuild.exe) в новый инструмент сборки (MSBuild.exe). Так как в Visual Studio 2010 применяется другой инструмент сборки, эта проблема может происходить при каждой миграции проекта из версии до 2010 в версию 2010 или более позднюю. Основная проблема заключается в том, что мастер миграции проекта не обновляет значение Link.OutputFile , так как не всегда возможно определить, какое его значение должно быть основано на других параметрах проекта. Поэтому его обычно приходится устанавливать вручную. Дополнительные сведения см. в публикации в блоге Visual C++.

В этом случае для свойства Link.OutputFile в преобразованном проекте было установлено значение Debug\Spyxx.exe и \Release\Spyxx.exe для проекта Spy ++ в зависимости от конфигурации. Лучше всего просто заменить эти жестко запрограммированные значения на $(TargetDir)$(TargetName)$(TargetExt) для всех конфигураций. Если это не работает, можно настроить его или изменить свойства в разделе "Общие ", где задаются эти значения (свойства — выходной каталог, имя целевого объекта и расширение целевого объекта. Помните, что если просматриваемое вами свойство использует макросы, то в раскрывающемся списке можно нажать Изменить, чтобы открыть диалоговое окно, где будет отображаться последняя строка со сделанными подстановками макросов. Для просмотра всех доступных макросов и их текущих значений нажмите кнопку Макросы.

Шаг 4. Обновление целевой версии Windows

Следующая ошибка указывает, что версия WINVER больше не поддерживается в MFC. Версия WINVER для Windows XP — 0x0501.

C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxv_w32.h(40): fatal error C1189: #error:  MFC does not support WINVER less than 0x0501.  Please change the definition of WINVER in your project properties or precompiled header.

Корпорация Майкрософт больше не поддерживает Windows XP, поэтому, хотя эту ОС можно использовать в качестве целевой в Visual Studio, рекомендуем вам постепенно отказываться от ее поддержки в своих приложениях и просить своих клиентов переходить на новые версии Windows.

Для устранения ошибки определите WINVER путем обновления свойств проекта — укажите здесь самую раннюю версию Windows, на которую вы хотите в настоящее время ориентироваться. Таблицу значений для различных версий Windows см. здесь.

Файл stdafx.h содержал некоторые из этих определений макросов.

#define WINVER       0x0500  // these defines are set so that we get the
#define _WIN32_WINNT 0x0500  // maximum set of message/flag definitions,
#define _WIN32_IE    0x0400  // from both winuser.h and commctrl.h.

Для WINVER мы установим Windows 7. Проще прочитать код позже, если вы используете макрос для Windows 7 (_WIN32_WINNT_WIN7), а не само значение (0x0601).

#define WINVER _WINNT_WIN32_WIN7 // Minimum targeted Windows version is Windows 7

Шаг 5. Ошибки компоновщика

Благодаря этим изменениям проект SpyHk (DLL) выполняет сборку, но создает ошибку компоновщика.

LINK : warning LNK4216: Exported entry point _DLLEntryPoint@12

Точка входа для библиотеки DLL не должна экспортироваться. Точка входа должна вызываться загрузчиком только при первой загрузке библиотеки DLL в память, поэтому ее не должно быть в таблице экспорта, предназначенной для других вызывающих объектов. Нам нужно просто убедиться, что к ней не присоединена директива __declspec(dllexport). В файле spyxxhk.c ее необходимо удалить в двух местах: в объявлении и определении DLLEntryPoint. Смысла использовать эту директиву никогда не было, но предыдущие версии компоновщика и компилятора не отмечали ее как проблему. Более новые версии компоновщика выдают предупреждение.

// deleted __declspec(dllexport)
BOOL WINAPI DLLEntryPoint(HINSTANCE hinstDLL,DWORD fdwReason, LPVOID lpvReserved);

Теперь проект C DLL (SpyHK.dll) выполняет процесс сборки и компоновки без ошибок.

Шаг 6. Более устаревшие файлы заголовков

Теперь мы приступаем к работе с основным проектом исполняемого файла — Spyxx.

Несколько других включаемых файлов (ctl3d.h и penwin.h) найти не удалось. Хотя это может быть полезно для поиска в Интернете, чтобы попытаться определить, что включало заголовок, иногда информация не так полезна. Мы обнаружили, что файл ctl3d.h входил в состав Exchange Development Kit и обеспечивал поддержку определенного стиля элементов управления в Windows 95, а penwin.h относится к технологии вычислений с использованием перьевого указателя (устаревшего API). В этом случае мы просто закомментируем строку #include и займемся неопределенными символами, как мы сделали с файлом verstamp.h. Все, что относится к технологиям 3D Controls или к компьютерам с перьевым вводом, было удалено из проекта.

В случае проекта с большим количеством ошибок компиляции, которые вы постепенно устраняете, нереально найти все случаи использования устаревшего API сразу после удаления директивы #include. Мы не обнаружили все сразу, а лишь через некоторое время выявили ошибку, которая заключается в том, что символ WM_DLGBORDER не был определен. Фактически это один из многочисленных неопределенных символов, которые содержатся в файле ctl3d.h. Определив, что данный символ относится к устаревшему API, мы удалили все ссылки на него в коде.

Шаг 7. Обновление старого кода iostream

Следующая ошибка чаще всего происходит со старым кодом C++, который использует потоки iostream.

mstream.h(40): fatal error C1083: Cannot open include file: 'iostream.h': No such file or directory

Проблема заключается в том, что старая библиотека iostream была удалена и заменена. Старые iostream необходимо заменить на новые стандарты.

#include <iostream.h>
#include <strstrea.h>
#include <iomanip.h>

Обновления включают в себя:

#include <iostream>
#include <sstream>
#include <iomanip>

Из-за этого изменения возникают проблемы с ostrstream, который больше не используется. Соответствующая замена — ostringstream. Мы пытаемся добавить код typedef , ostrstream чтобы избежать изменения кода слишком много, по крайней мере, как начало.

typedef std::basic_ostringstream<TCHAR> ostrstream;

Сейчас компоновка проекта выполняется с помощью MBCS (многобайтовой кодировки), поэтому соответствующим символьным типом данных является char. Тем не менее, чтобы упростить обновление кода до набора символов Юникод UTF-16, мы обновляем его до TCHAR, который разрешается в char или wchar_t в зависимости от того, какое значение установлено для свойства Кодировка в параметрах проекта — MBCS или Юникод.

Необходимо обновить несколько фрагментов кода. Мы заменили базовый класс iosios_baseна , и мы заменили ostream на basic_ostream<T>. Мы добавляем два дополнительных typedef и компилируем данный раздел.

typedef std::basic_ostream<TCHAR> ostream;
typedef ios_base ios;

Использование этих typedef представляет собой просто временное решение. Чтобы получить постоянное решение, следует обновить каждую ссылку на переименованный или устаревший API.

Ниже приведена следующая ошибка.

error C2039: 'freeze': is not a member of 'std::basic_stringbuf<char,std::char_traits<char>,std::allocator<char>>'

Следующая проблема заключается в том, что basic_stringbuf у него нет freeze метода. Метод freeze используется для предотвращения утечки памяти в старом ostream. Мы не нуждаемся в нем сейчас, когда мы используем новый ostringstream. Мы можем удалить вызов freeze.

//rdbuf()->freeze(0);

Следующие две ошибки произошли в соседних строках. Первый жалуется на использование ends, который является манипулятором ввода-вывода старой iostream библиотеки, который добавляет конца null в строку. Вторая из этих ошибок объясняет, что выходные данные str метода не могут быть назначены указателю, отличному от const.

// Null terminate the string in the buffer and
// get a pointer to it.
//
*this << ends;
LPSTR psz = str();
2>mstream.cpp(167): error C2065: 'ends': undeclared identifier2>mstream.cpp(168): error C2440: 'initializing': cannot convert from 'std::basic_string<char,std::char_traits<char>,std::allocator<char>>' to 'LPSTR'

Благодаря применению новой библиотеки потоков ends не нужен, так как строка всегда имеет знак завершения NULL, поэтому ее можно удалить. Во-вторых, проблема заключается в том, что теперь str() не возвращает указатель на массив символов для строки; он возвращает std::string тип. Для устранения второй проблемы следует изменить тип на LPCSTR и использовать метод c_str() для запроса указателя.

//*this << ends;
LPCTSTR psz = str().c_str();

В этом коде произошла ошибка, которая озадачила нас на некоторое время.

MOUT << _T(" chUser:'") << chUser
<< _T("' (") << (INT)(UCHAR)chUser << _T(')');

Макрос MOUT разрешается в *g_pmout, который является объектом типа mstream. Класс mstream является производным от класса стандартной выходной строки, std::basic_ostream<TCHAR>. Однако при использовании _T вокруг строкового литерала, который мы помещаем в подготовку к преобразованию в Юникод, разрешение перегрузки для оператора << завершается ошибкой со следующим сообщением об ошибке:

1>winmsgs.cpp(4612): error C2666: 'mstream::operator <<': 2 overloads have similar conversions
1>  c:\source\spyxx\spyxx\mstream.h(120): note: could be 'mstream &mstream::operator <<(ios &(__cdecl *)(ios &))'
1>  c:\source\spyxx\spyxx\mstream.h(118): note: or       'mstream &mstream::operator <<(ostream &(__cdecl *)(ostream &))'
1>  c:\source\spyxx\spyxx\mstream.h(116): note: or       'mstream &mstream::operator <<(ostrstream &(__cdecl *)(ostrstream &))'
1>  c:\source\spyxx\spyxx\mstream.h(114): note: or       'mstream &mstream::operator <<(mstream &(__cdecl *)(mstream &))'
1>  c:\source\spyxx\spyxx\mstream.h(109): note: or       'mstream &mstream::operator <<(LPTSTR)'
1>  c:\source\spyxx\spyxx\mstream.h(104): note: or       'mstream &mstream::operator <<(TCHAR)'
1>  c:\source\spyxx\spyxx\mstream.h(102): note: or       'mstream &mstream::operator <<(DWORD)'
1>  c:\source\spyxx\spyxx\mstream.h(101): note: or       'mstream &mstream::operator <<(WORD)'
1>  c:\source\spyxx\spyxx\mstream.h(100): note: or       'mstream &mstream::operator <<(BYTE)'
1>  c:\source\spyxx\spyxx\mstream.h(95): note: or       'mstream &mstream::operator <<(long)'
1>  c:\source\spyxx\spyxx\mstream.h(90): note: or       'mstream &mstream::operator <<(unsigned int)'
1>  c:\source\spyxx\spyxx\mstream.h(85): note: or       'mstream &mstream::operator <<(int)'
1>  c:\source\spyxx\spyxx\mstream.h(83): note: or       'mstream &mstream::operator <<(HWND)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1132): note: or       'CDumpContext &operator <<(CDumpContext &,COleSafeArray &)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1044): note: or       'CArchive &operator <<(CArchive &,ATL::COleDateTimeSpan)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1042): note: or       'CDumpContext &operator <<(CDumpContext &,ATL::COleDateTimeSpan)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1037): note: or       'CArchive &operator <<(CArchive &,ATL::COleDateTime)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1035): note: or       'CDumpContext &operator <<(CDumpContext &,ATL::COleDateTime)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1030): note: or       'CArchive &operator <<(CArchive &,COleCurrency)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1028): note: or       'CDumpContext &operator <<(CDumpContext &,COleCurrency)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(955): note: or       'CArchive &operator <<(CArchive &,ATL::CComBSTR)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(951): note: or       'CArchive &operator <<(CArchive &,COleVariant)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(949): note: or       'CDumpContext &operator <<(CDumpContext &,COleVariant)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxwin.h(248): note: or       'CArchive &operator <<(CArchive &,const RECT &)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxwin.h(247): note: or       'CArchive &operator <<(CArchive &,POINT)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxwin.h(246): note: or       'CArchive &operator <<(CArchive &,SIZE)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxwin.h(242): note: or       'CDumpContext &operator <<(CDumpContext &,const RECT &)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxwin.h(241): note: or       'CDumpContext &operator <<(CDumpContext &,POINT)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxwin.h(240): note: or       'CDumpContext &operator <<(CDumpContext &,SIZE)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afx.h(1639): note: or       'CArchive &operator <<(CArchive &,const CObject *)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afx.h(1425): note: or       'CArchive &operator <<(CArchive &,ATL::CTime)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afx.h(1423): note: or       'CDumpContext &operator <<(CDumpContext &,ATL::CTime)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afx.h(1418): note: or       'CArchive &operator <<(CArchive &,ATL::CTimeSpan)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afx.h(1416): note: or       'CDumpContext &operator <<(CDumpContext &,ATL::CTimeSpan)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\ostream(694): note: or       'std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &std::operator <<<wchar_t,std::char_traits<wchar_t>>(std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &,const char *)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\ostream(741): note: or       'std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &std::operator <<<wchar_t,std::char_traits<wchar_t>>(std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &,char)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\ostream(866): note: or       'std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &std::operator <<<wchar_t,std::char_traits<wchar_t>>(std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &,const _Elem *)'
1>          with
1>          [
1>              _Elem=wchar_t
1>          ]
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\ostream(983): note: or       'std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &std::operator <<<wchar_t,std::char_traits<wchar_t>,wchar_t[10]>(std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &&,const _Ty (&))'
1>          with
1>          [
1>              _Ty=wchar_t [10]
1>          ]
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\ostream(1021): note: or       'std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &std::operator <<<wchar_t,std::char_traits<wchar_t>>(std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &,const std::error_code &)'
1>  winmsgs.cpp(4612): note: while trying to match the argument list '(CMsgStream, const wchar_t [10])'

Существует так много определений операторов << , что такая ошибка может быть страшной. После более внимательного анализа доступных перегрузок можно увидеть, что большинство из них являются нерелевантными. Благодаря более глубокому анализу определения класса mstream мы обнаружили следующую функцию, которая, как нам кажется, должна вызываться в этом случае.

mstream& operator<<(LPTSTR psz)
{
  return (mstream&)ostrstream::operator<<(psz);
}

Причина, по которой она не вызывается, заключается в том, что строковый литерал имеет тип const wchar_t[10], как видно из последней строки в этом длинном сообщении об ошибке, поэтому преобразование в указатель, отличный от const, не выполняется автоматически. Однако этот оператор не должен изменять входной параметр, поэтому более подходящим типом параметра является LPCTSTR (const char* при компиляции как MBCS и const wchar_t* — как Юникод), а не LPTSTR (char* при компиляции как MBCS и wchar_t* — как Юникод). Если внести данное изменение, ошибка будет устранена.

Этот тип преобразования был разрешен в более старом и менее строгом компиляторе, но для более поздних изменений, обеспечивающих совместимость, требуется более правильный код.

Шаг 8. Более строгие преобразования компилятора

Мы также получаем много примерно следующих ошибок:

error C2440: 'static_cast': cannot convert from 'UINT (__thiscall CHotLinkCtrl::* )(CPoint)' to 'LRESULT (__thiscall CWnd::* )(CPoint)'

Ошибка произошла в схеме сообщений, которая является просто макросом:

BEGIN_MESSAGE_MAP(CFindToolIcon, CWnd)
// other messages omitted...
ON_WM_NCHITTEST() // Error occurs on this line.
END_MESSAGE_MAP()

Если посмотреть на определение этого макроса, мы увидим, что он ссылается на функцию OnNcHitTest.

#define ON_WM_NCHITTEST() \
{ WM_NCHITTEST, 0, 0, 0, AfxSig_l_p, \
(AFX_PMSG)(AFX_PMSGW) \
(static_cast< LRESULT (AFX_MSG_CALL CWnd::*)(CPoint) > (&ThisClass :: OnNcHitTest)) },

Проблема связана с несоответствием в указателе на типы функций-членов. Проблема не является преобразованием из CHotLinkCtrl типа класса в тип класса в качестве типа CWnd класса, так как это допустимое преобразование на основе. Проблема — тип возвращаемого значения: UINT и LRESULT. LRESULT разрешается в LONG_PTR, который является 64- или 32-разрядным указателем в зависимости от целевого двоичного типа, поэтому UINT не выполняет преобразование в этот тип. Такая ситуация при обновлении кода, написанного до 2005 г., является довольно редкой, так как тип возвращаемого значения многих методов схемы сообщений изменился с UINT на LRESULT в Visual Studio 2005 в рамках изменений для обеспечения совместимости с 64-разрядной версией. В следующем коде мы изменяем тип возвращаемого значения с UINT на LRESULT:

afx_msg UINT OnNcHitTest(CPoint point);

После изменения получается следующий код:

afx_msg LRESULT OnNcHitTest(CPoint point);

Так как существует около десяти вхождения этой функции во всех разных классах, производных от CWnd, полезно использовать go to Definition (Клавиатура: F12) и Перейти к объявлению (клавиатура: клавиша CTRL+ F12), когда курсор находится в функции в редакторе, чтобы найти их и перейти к ним из окна инструментов поиска символов. Обычно функция Перейти к определению является более полезной при поиске. Перейти к объявлению позволяет искать объявления, отличные от объявления определяющего класса, например объявления дружественного класса или ссылки вперед.

Шаг 9. Изменения в MFC

Следующая ошибка также связана с типом измененного объявления и также возникает в макросе.

error C2440: 'static_cast': cannot convert from 'void (__thiscall CFindWindowDlg::* )(BOOL,HTASK)' to 'void (__thiscall CWnd::* )(BOOL,DWORD)'

Проблема заключается в том, что второй параметр CWnd::OnActivateApp изменен с HTASK на DWORD. Это изменение было сделано в выпуске 2002 г. для Visual Studio, Visual Studio .NET.

afx_msg void OnActivateApp(BOOL bActive, HTASK hTask);

Нам нужно обновить объявления OnActivateApp в производных классах следующим образом:

afx_msg void OnActivateApp(BOOL bActive, DWORD dwThreadId);

Теперь мы можем скомпилировать проект. Но у нас есть несколько предупреждений для проработки, а также имеются вспомогательные части обновления, например преобразование из MBCS в Юникод или повышение безопасности с помощью функций Secure CRT.

Шаг 10. Обработка предупреждений компилятора

Чтобы получить полный список предупреждений, выполните задачу Перестроить все в решении вместо обычного построения, чтобы убедиться, что все ранее скомпилированные части будут перекомпилированы, так как отчеты с предупреждениями будут поступать только из текущей компиляции. Другой вопрос заключается в том, следует ли оставить текущий уровень предупреждений или использовать более высокий уровень предупреждений. При переносе большого объема кода, особенно старого кода, более удобным вариантом может оказаться применение более высокого уровня предупреждений. Также можно начать с уровня предупреждений по умолчанию, а затем повысить уровень, чтобы получать все предупреждения. При использовании /Wall вы получаете некоторые предупреждения в системных файлах заголовков, поэтому многие пользователи применяют /W4 для получения большинства предупреждений для своего кода вместо получения предупреждений для системных файлов заголовков. Если требуется, чтобы предупреждения отображались как ошибки, добавьте параметр /WX. Эти параметры находятся в разделе C/C++ диалогового окна Свойства проекта.

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

void SetDialogBkColor() {CWinApp::SetDialogBkColor(::GetSysColor(COLOR_BTNFACE));}

Предупреждение выглядит следующим образом.

warning C4996: 'CWinApp::SetDialogBkColor': CWinApp::SetDialogBkColor is no longer supported. Instead, handle WM_CTLCOLORDLG in your dialog

Сообщение WM_CTLCOLORDLG уже было обработано в коде Spy++, поэтому нужно было просто удалить все ссылки на SetDialogBkColor, который больше не требуется.

Для устранения следующего предупреждения нужно было просто закомментировать имя переменной. Мы получили следующее предупреждение:

warning C4456: declaration of 'lpszBuffer' hides previous local declaration

Код, создающий данное предупреждение, содержит макрос.

DECODEPARM(CB_GETLBTEXT)
{
  P2WPOUT();

  P2LPOUTPTRSTR;
  P2IFDATA()
  {
    PARM(lpszBuffer, PPACK_STRINGORD, ED2);

    INDENT();

    P2IFISORD(lpszBuffer)
    {
      P2OUTORD(lpszBuffer);
    }
    else
    {
      PARM(lpszBuffer, LPTSTR, ED2);
      P2OUTS(lpszBuffer);
    }
  }
}

Активное применение макросов, как в этом коде, может усложнить работу с самим кодом. В этом случае макросы содержат объявления переменных. Макрос PARM определяется следующим образом:

#define PARM(var, type, src)type var = (type)src

Поэтому переменная lpszBuffer объявляется в одной и той же функции два раза. Если бы в коде макросы не использовались, то такую ошибку исправить было бы очень просто (нужно просто удалить второе объявление типа). Однако у нас другая ситуация, и мы вынуждены решить, нужно ли переписать код макроса как обычный код (что является трудоемкой задачей, сопряженной с ошибками) или отключить предупреждение.

В этом случае мы решили отключить предупреждение. Это можно сделать путем добавления директивы pragma следующим образом:

#pragma warning(disable : 4456)

При отключении предупреждения может потребоваться ограничить эффект отключения только в том коде, который создает предупреждение, чтобы избежать подавления предупреждения в тех случаях, когда оно может предоставлять полезные сведения. Мы добавляем код для восстановления предупреждения сразу после строки, которая выдает предупреждение. Но так как это предупреждение возникает в макросе, следует использовать ключевое слово __pragma, которое работает в макросах (#pragma в макросах не работает).

#define PARM(var, type, src)__pragma(warning(disable : 4456))  \
type var = (type)src \
__pragma(warning(default : 4456))

Следующее предупреждение требует внесения некоторых изменений в код. Win32 API GetVersionGetVersionEx) является устаревшим.

warning C4996: 'GetVersion': was declared deprecated

Следующий код показывает, как получить версию.

// check Windows version and set m_bIsWindows9x/m_bIsWindows4x/m_bIsWindows5x flags accordingly.
DWORD dwWindowsVersion = GetVersion();

Далее следует объемный код, который проверяет значение dwWindowsVersion, чтобы определить, работаем ли мы в Windows 95, а также версию Windows NT. Так как все это уже устарело, мы удаляем код и работаем со всеми ссылками на эти переменные.

Дополнительные сведения см. в статье Operating system version changes in Windows 8.1 and Windows Server 2012 R2 (Изменения версии операционной системы в Windows 8.1 и Windows Server 2012 R2).

Существуют методы в классе CSpyApp, которые запрашивают версию операционной системы: IsWindows9x, IsWindows4x и IsWindows5x. Для начала предположим, что все версии Windows, которые мы собираемся поддерживать (Windows 7 и более поздние версии) «близки» к Windows NT 5 с точки зрения технологий, используемых этим старым приложением. Применение данных методов было связано с ограничениями старых операционных систем. Поэтому мы изменили эти методы для возврата значения TRUE для IsWindows5x и значения FALSE для других.

BOOL IsWindows9x() {/*return(m_bIsWindows9x);*/ return FALSE;  }
BOOL IsWindows4x() {/*return(m_bIsWindows4x);*/ return FALSE;  }
BOOL IsWindows5x() {/*return(m_bIsWindows5x);*/ return TRUE;  }

В коде осталось лишь несколько мест, где внутренние переменные использовались напрямую. Так как мы удалили эти переменные, мы получили несколько сообщений об ошибках, которые нужно обработать явным образом.

error C2065: 'm_bIsWindows9x': undeclared identifier
void CSpyApp::OnUpdateSpyProcesses(CCmdUI *pCmdUI)
{
  pCmdUI->Enable(m_bIsWindows9x || hToolhelp32 != NULL);
}

Мы могли заменить это на вызов метода или просто передать значение TRUE и удалить старый особый случай для Windows 9x.

void CSpyApp::OnUpdateSpyProcesses(CCmdUI *pCmdUI)
{
  pCmdUI->Enable(TRUE /*!m_bIsWindows9x || hToolhelp32 != NULL*/);
}

Окончательное предупреждение на уровне по умолчанию (3) относится к битовому полю.

treectl.cpp(1656): warning C4463: overflow; assigning 1 to bit-field that can only hold values from -1 to 0

Код, который запускает это предупреждение, выглядит следующим образом.

m_bStdMouse = TRUE;

Объявление m_bStdMouse указывает, что это битовое поле.

class CTreeListBox : public CListBox
{
  DECLARE_DYNCREATE(CTreeListBox)

  CTreeListBox();

  private:
  int ItemFromPoint(const CPoint& point);

  class CTreeCtl* m_pTree;
  BOOL m_bGotMouseDown : 1;
  BOOL m_bDeferedDeselection : 1;
  BOOL m_bStdMouse : 1;

Этот код был написан до того, как в Visual C++ начал поддерживаться встроенный тип bool. В таком коде boOL был typedef для int. Тип является типом intsigned , и битовое представление a signed int заключается в том, чтобы использовать первый бит в качестве знака бит, поэтому битовое поле типа int может быть интерпретировано как представляющее 0 или -1, вероятно, не то, что было предназначено.

Если посмотреть на данный код, нельзя понять, для чего используются эти битовые поля. Предполагалось ли сохранить небольшой размер объекта или же где-то применяется двоичный макет объекта? Мы изменили их на обычные элементы BOOL, так как не видели причин использовать битовое поле. Применение битовых полей для сохранения небольшого размера объекта не обязательно будет работать. Это зависит от того, как компилятор размещает тип.

Вы можете задаться вопросом, если использовать стандартный тип bool на протяжении всего будет полезно. Многие из старых шаблонов кода, таких как тип BOOL, были изобретены для решения проблем, которые позже были решены в стандартной версии C++, поэтому переход с BOOL на bool встроенный тип является лишь одним примером такого изменения, которое рекомендуется выполнить после первоначального запуска кода в новой версии.

После рассмотрения всех предупреждений, которые появляются на уровне по умолчанию (уровень 3), перейдем на уровень 4 и рассмотрим несколько дополнительных предупреждений. Первое такое предупреждение имеет следующий вид:

warning C4100: 'nTab': unreferenced formal parameter

Код, который создал это предупреждение, имел следующий вид.

virtual void OnSelectTab(int nTab) {};

Код кажется достаточно безопасным, но так как нам требуется чистая компиляция с заданными параметрами /W4 и /WX, мы просто закомментируем имя переменной, оставив ее для удобочитаемости.

virtual void OnSelectTab(int /*nTab*/) {};

Другие предупреждения, которые мы получили, полезны для обычной очистки кода. Существует ряд неявных преобразований из int WORD unsigned int (который является типизированным для unsigned short). Они могут вызывать потерю данных. В таких случаях мы добавили приведение в WORD.

Другое предупреждение на уровне 4, полученное для этого кода:

warning C4211: nonstandard extension used: redefined extern to static

Проблема возникает, когда переменная была сначала объявлена extern, а затем объявлена static. Значения этих двух спецификаторов класса хранения являются взаимоисключающими, но это допускается как расширение Microsoft. Если требуется, чтобы код был переносимым в другие компиляторы, или его необходимо компилировать с параметром /Za (для совместимости с ANSI), нужно изменить объявления так, чтобы они соответствовали спецификаторам классов хранения.

Шаг 11. Перенос из MBCS в Юникод

Обратите внимание, что в мире Windows, когда мы говорим Юникод, обычно это означает UTF-16. В других операционных системах, например в Linux, применяется UTF-8, а в Windows обычно нет. MBCS-версия MFC была признана нерекомендуемой в Visual Studio 2013 и 2015, однако не является таковой в Visual Studio 2017. При использовании Visual Studio 2013 или 2015 перед фактическим переносом кода MBCS в Юникод UTF-16 может потребоваться временное устранение предупреждений о нежелательности применения MBCS, чтобы выполнить другую работу или отложить перенос на более удобное время. В текущем коде применяется MBCS, и для продолжения работы с ним нужно установить версию ANSI/MBCS для MFC. Достаточно крупная библиотека MFC не входит в вариант установки Visual Studio по умолчанию Разработка классических приложений на C++, поэтому ее нужно выбрать в разделе дополнительных компонентов в установщике. См. статью MFC MBCS DLL Add-on (Надстройка DLL MBCS MFC). После загрузки библиотеки и перезапуска Visual Studio можно выполнить компиляцию и компоновку с помощью версии MBCS MFC, но, чтобы избавиться от предупреждений о MBCS при использовании Visual Studio 2013 или 2015, запись NO_WARN_MBCS_MFC_DEPRECATION также следует добавить в список предварительно определенных макросов в разделе Препроцессор в свойствах проекта либо в начале файла заголовка stdafx.h или другого общего файла заголовка.

Теперь мы получили несколько сообщений об ошибках компоновщика.

fatal error LNK1181: cannot open input file 'mfc42d.lib'

Сообщение LNK1181 появляется потому, что устаревшая версия статической библиотеки для mfc включена во входные данные компоновщика. Это больше не обязательно, так как мы можем динамически связать MFC, поэтому нам просто нужно удалить все статические библиотеки MFC из свойства Input в разделе Компоновщика свойств проекта. В этом проекте также используется параметр /NODEFAULTLIB, который вместо этого перечисляет все зависимости библиотеки.

msvcrtd.lib;msvcirtd.lib;kernel32.lib;user32.lib;gdi32.lib;advapi32.lib;Debug\SpyHk55.lib;%(AdditionalDependencies)

Теперь давайте фактически обновим старый код MBCS в Юникод. Так как это приложение Windows тесно связано с платформой рабочего стола Windows, мы будет переносить его в Юникод UTF-16, используемый в Windows. При написании кроссплатформенного кода или переноса приложения Windows на другую платформу может потребоваться рассмотреть задачу переноса в UTF-8, которая широко применяется в других операционных системах.

При переносе в Юникод UTF-16 необходимо решить, сохранить ли возможность компиляции в MBCS или нет. Если требуется сохранить поддержку MBCS, в качестве типа символа нужно использовать макрос TCHAR, который разрешается в char или wchar_t в зависимости от того, определен ли _MBCS или _UNICODE во время компиляции. Переключение на TCHAR и версии TCHAR различных API, а не на wchar_t и его соответствующие API означает, что вы сможете просто вернуться к версии MBCS своего кода путем простого определения макроса _MBCS вместо _UNICODE. В дополнение к TCHAR существует несколько версий TCHAR, например широко используемые определения typedef, макросы и функции. Например, LPCTSTR вместо LPCSTR и т. д. В диалоговом окне свойств проекта в разделе Свойства конфигурации в подразделе Общие установите для свойства Кодировка вместо значения Использовать набор символов MBCS значение Использовать набор символов Юникода. Этот параметр влияет на то, какой макрос предварительно определен во время компиляции. Существуют два макроса: UNICODE и _UNICODE. Свойство проекта влияет на них единообразно. В заголовках Windows используется Юникод там, где заголовки Visual C++, например MFC, используют _UNICODE. Но, если один макрос определен, второй будет определен всегда.

Дополнительные сведения о переносе из MBCS в Юникод UTF-16 с помощью TCHAR см. в руководстве. Мы выбрали следующий путь. Сначала для свойства Кодировка мы установили значение Использовать набор символов Юникода и повторно построили проект.

В некоторых частях кода уже использовался TCHAR, очевидно с учетом поддержки в будущем Юникода. В других частях он не использовался. Мы искали экземпляры CHAR, для которых используется typedefchar, и заменили большинство из них на TCHAR. Кроме того, выполнили поиск sizeof(CHAR). Каждый раз, когда мы заменяли CHAR на TCHAR, нам обычно было нужно изменять на sizeof(TCHAR), так как он часто использовался для определения количества символов в строке. Использование здесь неправильного типа не будет вызывать ошибку компилятора, поэтому данный случай рекомендуется рассмотреть более подробно.

Ошибки такого типа довольно часто происходят сразу после переключения на Юникод.

error C2664: 'int wsprintfW(LPWSTR,LPCWSTR,...)': cannot convert argument 1 from 'CHAR [16]' to 'LPWSTR'

Ниже приведен пример кода, который создает следующее:

wsprintf(szTmp, "%d.%2.2d.%4.4d", rmj, rmm, rup);

Мы поместили _T вокруг строкового литерала, чтобы устранить ошибку.

wsprintf(szTmp, _T("%d.%2.2d.%4.4d"), rmj, rmm, rup);

Макрос _T выполняет компиляцию строкового литерала как строки char или wchar_t в зависимости от настроек MBCS или UNICODE. Чтобы заменить все строки с _T в Visual Studio, откройте окно Быстрая замена (сочетание клавиш CTRL+F) или Заменить в файлах (сочетание клавиш CTRL+SHIFT+H), а затем установите флажок Использовать регулярные выражения. Введите ((\".*?\")|('.+?')) как искомый текст и _T($1) как текст для замены. Если вокруг некоторых строк макрос _T уже имеется, эта процедура добавит его снова. В коде также могут быть случаи, где вы не хотите использовать _T. Например, если используется #include, то лучше всего использовать Заменить следующий, а не Заменить все.

В заголовках Windows фактически определена функция wsprintf, но в документации ее использовать не рекомендуется, так как она может вызвать переполнение буфера. Для буфера szTmp размер не задан, поэтому функция не может проверить, сможет ли буфер вместить все данные, подлежащие записи в него. В следующем разделе описывается перенос в Secure CRT, в котором мы будем устранять другие подобные проблемы. Мы завершили процедуру путем замены функции на _stprintf_s.

Еще одна распространенная ошибка, которую вы увидите при преобразовании в Юникод.

error C2440: '=': cannot convert from 'char *' to 'TCHAR *'

Код, создающий данное сообщение об ошибке, выглядит следующим образом:

pParentNode->m_szText = new char[strTitle.GetLength() + 1];
_tcscpy(pParentNode->m_szText, strTitle);

Несмотря на то, что _tcscpy была использована функция TCHAR strcpy для копирования строки, выделенный буфером был char буфер. Его легко изменить на TCHAR.

pParentNode->m_szText = new TCHAR[strTitle.GetLength() + 1];
_tcscpy(pParentNode->m_szText, strTitle);

Аналогичным образом при появлении сообщения об ошибке компилятора мы изменили LPSTR (Long Pointer to STRing — длинный указатель на строку) и LPCSTR (Long Pointer to Constant STRing — длинный указатель на константную строку) на LPTSTR (Long Pointer to TCHAR STRing — длинный указатель на строку TCHAR) и LPCTSTR (Long Pointer to Constant TCHAR STRing — длинный указатель на константную строку TCHAR) соответственно. Мы решили не выполнять такую замену с помощью функции глобального поиска и замены, так как каждую ситуацию было нужно анализировать по отдельности. В некоторых случаях char требуется версия, например при обработке определенных сообщений Windows, использующих структуры Windows с суффиксом A . В Windows API суффикс A означает ASCII или ANSI (также применяется к MBCS), а суффикс W означает расширенные символы или Юникод UTF-16. Этот шаблон именования используется в заголовках Windows, но мы также применяли его в коде Spy++, когда нам было нужно добавить версию Юникода для функции, который уже была определен только в версии MBCS.

В некоторых случаях нам пришлось заменить тип для использования версии, которая корректно разрешается (например, WNDCLASS вместо WNDCLASSA).

Во многих случаях нам было необходимо использовать универсальную версию (макрос) API-интерфейса Win32, например GetClassName (вместо GetClassNameA). В операторе switch обработчика сообщений некоторые сообщения специфичны для MBCS или Юникода. В таких случаях нам было необходимо изменить код для явного вызова версии MBCS, так как мы заменили универсально именованные функции на функции, специфичные для A и W, и добавили макрос для универсального имени, которое разрешается в правильное имя A или W в зависимости от того, определен ли ЮНИКОД. Во многих частях кода при переключении для определения _UNICODE версия W теперь выбирается даже в том случае, когда требуется версия A.

В нескольких частях кода потребовалось выполнить специальные действия. Использовать WideCharToMultiByte или MultiByteToWideChar нужно с осторожностью. Ниже приведен пример применения WideCharToMultiByte.

BOOL C3dDialogTemplate::GetFont(CString& strFace, WORD& nFontSize)
{
  ASSERT(m_hTemplate != NULL);

  DLGTEMPLATE* pTemplate = (DLGTEMPLATE*)GlobalLock(m_hTemplate);
  if ((pTemplate->style & DS_SETFONT) == 0)
  {
    GlobalUnlock(m_hTemplate);
    return FALSE;
  }

  BYTE* pb = GetFontSizeField(pTemplate);
  nFontSize = *(WORD*)pb;
  pb += sizeof (WORD);
  WideCharToMultiByte(CP_ACP, 0, (LPCWSTR)pb, -1,
  strFace.GetBufferSetLength(LF_FACESIZE), LF_FACESIZE, NULL, NULL);
  strFace.ReleaseBuffer();
  GlobalUnlock(m_hTemplate);
  return TRUE;
}

Для решения этой проблемы следовало понять, что причиной таких действий была необходимость копирования широкой строки символов, представляющей имя шрифта во внутреннем буфере строки CString, strFace. Это потребовало создания немного другого кода для многобайтовых строк CString как для строк CString с расширенными символами, поэтому в данном случае мы добавили #ifdef.

#ifdef _MBCS
WideCharToMultiByte(CP_ACP, 0, (LPCWSTR)pb, -1,
strFace.GetBufferSetLength(LF_FACESIZE), LF_FACESIZE, NULL, NULL);
strFace.ReleaseBuffer();
#else
wcscpy(strFace.GetBufferSetLength(LF_FACESIZE), (LPCWSTR)pb);
strFace.ReleaseBuffer();
#endif

Разумеется, вместо wcscpy мы должны были использовать более безопасную версию wcscpy_s. Эта задача рассматривается в следующем разделе.

В качестве проверки своей работы мы должны сбросить кодировку на значение Использовать многобайтовую кодировку, а также убедиться в том, что код по-прежнему компилируется с помощью MBCS и Юникода. Следует отметить, что после внесения всех этих изменений для перекомпилированного приложения нужно было выполнить полное тестирование.

В нашей работе с данным решением Spy++ среднестатистический разработчик C++ потратил бы около двух рабочих дней на преобразование кода в Юникод. Причем сюда не входит время на повторное тестирование.

Шаг 12. Перенос для использования Secure CRT

Рассмотрим задачу переноса кода, чтобы использовать безопасные версии функций CRT (версии с суффиксом _s). В этом случае общая стратегия состоит в замене функции на версию _s, а затем, как правило, в добавлении необходимых дополнительных параметров размера буфера. В большинстве случаев сделать это несложно, так как размер известен. В других случаях, когда размер недоступен немедленно, необходимо добавить дополнительные параметры в функцию, использующую функцию CRT, или, возможно, проверить использование целевого буфера и узнать, какие ограничения имеют соответствующие размеры.

Visual C++ позволяет упростить процесс создания безопасного кода без добавления большого количества параметров размера, что достигается благодаря применению перегрузок шаблонов. Так как такие перегрузки являются шаблонами, они доступны только при компиляции как C++, а не как C. Spyxxhk — это проект C, поэтому данный прием для него работать не будет. А Spyxx — нет, поэтому описанный прием можно применить. Прием заключается в добавлении подобной строки там, где будет выполняться компиляция — в любом файле проекта, например в файле stdafx.h:

#define _CRT_SECURE_TEMPLATE_OVERLOADS 1

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

Возвращаемые значения некоторых функций изменяются. Например _itoa_s_itow_s и макрос _itot_s) возвращает код ошибки (errno_t), а не строку. Поэтому в таких случаях следует переместить вызов в _itoa_s на отдельную строку и заменить его на идентификатор буфера.

Некоторые из распространенных случаев: для memcpy при переключении на memcpy_s мы часто добавляли размер структуры, в которую выполняется копирование. Аналогичным образом, для большинства строк и буферов размер массива или буфера легко определить с помощью объявления буфера или путем поиска места, в котором изначально располагался буфер. Для некоторых ситуаций необходимо определить, насколько большой буфер доступен, и если эта информация недоступна в область функции, которую вы изменяете, ее следует добавить в качестве дополнительного параметра и вызывающий код следует изменить, чтобы предоставить информацию.

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

Шаг 13. /Zc:forScope использовать не рекомендуется.

Начиная с Visual C++ 6.0 компилятор соответствует текущему стандарту, который ограничивает область переменных, объявленных в цикле, самой областью цикла. Параметр компилятора /Zc: forScope (Принудительное обеспечение соответствия для области цикла в свойствах проекта) контролирует, сообщается ли о данной ситуации как об ошибке или нет. Мы обновили наш код для обеспечения соответствия и добавили объявления сразу за пределами цикла. Во избежание внесения изменений в код этот параметр можно изменить в разделе Язык свойств проекта C++ на No (/Zc:forScope-). Однако имейте в виду, что /Zc:forScope- может быть удален в будущих версиях Visual C++, поэтому в конечном итоге код будет необходимо изменить, чтобы он соответствовал стандарту.

Эти проблемы относительно несложно устранить, но, в зависимости от вашего кода, может потребоваться внесение изменений в большой объем кода. Ниже рассмотрена типичная проблема.

int CPerfTextDataBase::NumStrings(LPCTSTR mszStrings) const
{
  for (int n = 0; mszStrings[0] != 0; n++)
  mszStrings = _tcschr(mszStrings, 0) + 1;
  return(n);
}

Этот код создает ошибку:

'n': undeclared identifier

Это происходит из-за того, что компилятор не рекомендует параметр компилятора, разрешающий код, который больше не соответствует стандарту C++. В соответствии со стандартом объявление переменной в цикле ограничивает свою область только пределами цикла, поэтому для использования счетчика цикла вне цикла обычно требуется, чтобы объявление счетчика также было бы перенесено за пределы цикла, как показано в следующем измененном коде:

int CPerfTextDataBase::NumStrings(LPCTSTR mszStrings) const
{
  int n;
  for (n = 0; mszStrings[0] != 0; n++)
  mszStrings = _tcschr(mszStrings, 0) + 1;
  return(n);
}

Итоги

Процедура переноса Spy++ из исходного кода Visual C++ 6.0 в последний компилятор заняла около 20 часов всего процесса кодирования, который продолжался примерно одну неделю. Мы выполнили обновление непосредственно через восемь версий продукта, с Visual Studio 6.0 до Visual Studio 2015. Теперь это рекомендуемый подход для всех обновлений в больших и маленьких проектах.

См. также

Перенос и обновление: примеры и конкретные случаи
Previous case study: COM Spy (Предыдущий пример: COM Spy)