Потоки Windows: _beginthread против _beginthreadex против CreateThread C ++

133

Какой лучший способ начать обсуждение _beginthread, _beginthreadxили CreateThread?

Я пытаюсь определить, в чем преимущества / недостатки _beginthread, _beginthreadexи CreateThread. Все эти функции возвращают дескриптор потока для вновь созданного потока, я уже знаю, что CreateThread предоставляет небольшую дополнительную информацию при возникновении ошибки (ее можно проверить, позвонив GetLastError) ... но какие вещи я должен учитывать, когда я ' м используя эти функции?

Я работаю с приложением Windows, поэтому о кроссплатформенной совместимости уже не может быть и речи.

Я просмотрел документацию по msdn и просто не могу понять, например, почему кто-то решил использовать _beginthread вместо CreateThread или наоборот.

Ура!

Обновление: хорошо, спасибо за всю информацию, я также прочитал в нескольких местах, которые я не могу назвать, WaitForSingleObject()если бы использовал _beginthread(), но если я позвоню _endthread()в поток, не должно ли это работать? В чем дело?

Кирил
источник
2
Вот анализ того, что _beginthreadex () делает для программистов C / C ++, который я нашел по ссылке на веб-сайте Эли Бендерски. Это из вопросов и ответов о том, использовать ли CreateThread () или нет. microsoft.com/msj/0799/win32/win320799.aspx
Ричард Чемберс,

Ответы:

96

CreateThread() - это необработанный вызов Win32 API для создания еще одного потока управления на уровне ядра.

_beginthread()& _beginthreadex()- это вызовы библиотеки времени выполнения C, которые вызываются CreateThread()за кулисами. После CreateThread()возврата _beginthread/ex()занимается дополнительной бухгалтерией, чтобы сделать библиотеку времени выполнения C пригодной для использования и согласованной в новом потоке.

В C ++ вам почти наверняка следует использовать, _beginthreadex()если вы вообще не будете связываться с библиотекой времени выполнения C (также известной как MSVCRT * .dll / .lib).

Дрю Холл
источник
39
Это уже не так, как раньше. CRT будет правильно работать в потоке, созданном CreateThread (), за исключением функции signal (). Будет небольшая утечка памяти (~ 80 байт) для каждого потока, созданного с помощью CreateThread (), который использует CRT, но он будет работать правильно. См. Дополнительную информацию: support.microsoft.com/default.aspx/kb/104641
Джон Диблинг,
1
@John: На самом деле эта ошибка применима только до MSVC ++ 6.0
bobobobo
5
@bobobobo: Хороший вопрос. Я могу только предположить, что изначально MS планировала использовать _beginподпрограммы как внутренние вызовы и CreateThreadдолжна была быть функцией API, которую все вызовут. Другое возможное объяснение заключается в том, что MS имеет долгую и славную историю игнорирования стандарта и принятия очень плохих решений по поводу именования вещей.
Джон Диблинг,
15
Эти _beginфункции начинаются с подчеркивания , потому что Microsoft начал более внимательно следить за развитием стандарта. В среде выполнения C имена с подчеркиванием зарезервированы для реализации (и реализация может задокументировать их для использования конечным пользователем, как в случае с ними). beginthreadex()это имя, которое разрешено использовать пользователю. Если среда выполнения C использовала его, то он мог бы конфликтовать с символом конечного пользователя, который пользователь имел законное право ожидать использования. Обратите внимание, что API Win32 не являются частью среды выполнения C и используют пространство имен пользователя.
Michael Burr
2
@Lothar: существуют различия между вызовом Win32 API CreateThreadи вызовами CRT _beginthread/ex, и при вызове CRT в потоке его всегда следует создавать с помощью _beginthread/ex. Если вы этого не сделаете, утечек памяти может не быть. Но вы наверняка не получите правильную инициализацию среды с плавающей запятой CreateThread, например, при вызове . Там же еще : «Если поток создается с помощью CreateThread вызывает CRT, ЭЛТ может завершить процесс в условиях низкой памяти.»
IInspectable
37

Есть несколько различий между _beginthread()и _beginthreadex(). _beginthreadex()был сделан, чтобы действовать больше как CreateThread()(по обоим параметрам и как он себя ведет).

Как упоминает Дрю Холл , если вы используете среду выполнения C / C ++, вы должны использовать _beginthread()/ _beginthreadex()вместо, CreateThread()чтобы среда выполнения имела возможность выполнить инициализацию собственного потока (настройка локального хранилища потока и т. Д.).

На практике это означает, что это CreateThread()практически никогда не должно использоваться напрямую в вашем коде.

В документах MSDN для _beginthread()/ _beginthreadex()есть довольно много деталей о различиях - одним из наиболее важных является то, что, поскольку дескриптор потока для потока, созданного с помощью, _beginthread()автоматически закрывается CRT, когда поток завершается, "если поток, созданный _beginthread, завершается быстро дескриптор, возвращаемый вызывающей стороне _beginthread, может оказаться недопустимым или, что еще хуже, указывать на другой поток ".

Вот что _beginthreadex()говорят комментарии к источнику CRT:

Differences between _beginthread/_endthread and the "ex" versions:

1)  _beginthreadex takes the 3 extra parameters to CreateThread
  which are lacking in _beginthread():
    A) security descriptor for the new thread
    B) initial thread state (running/asleep)
    C) pointer to return ID of newly created thread

2)  The routine passed to _beginthread() must be __cdecl and has
  no return code, but the routine passed to _beginthreadex()
  must be __stdcall and returns a thread exit code.  _endthread
  likewise takes no parameter and calls ExitThread() with a
  parameter of zero, but _endthreadex() takes a parameter as
  thread exit code.

3)  _endthread implicitly closes the handle to the thread, but
  _endthreadex does not!

4)  _beginthread returns -1 for failure, _beginthreadex returns
  0 for failure (just like CreateThread).

Обновление в январе 2013 г .:

CRT для VS 2012 имеет дополнительный бит инициализации, выполняемый в _beginthreadex(): если процесс является «упакованным приложением» (если из него возвращается что-то полезное GetCurrentPackageId()), среда выполнения инициализирует MTA во вновь созданном потоке.

Майкл Берр
источник
3
Там являются соответствующие моменты , когда CreateThread () вполне обоснованны, но честно вам действительно нужно выйти из своего пути , чтобы сделать это. Мы говорим о полном отсутствии чего-либо переносимого и пишем исключительно WIN32 API DLL или приложение. Включая отсутствие вызовов среды выполнения C. Даже использование STL ограничено тем, что вы должны предоставить собственные распределители для использования функций управления памятью WIN32. Настройка для этого с помощью Developer Studio - это работа сама по себе, но для единственной библиотеки WIN32 с минимально возможным объемом памяти это можно сделать. Но да, это маловероятно почти для всех, кроме очень немногих.
WhozCraig
1
@WhozCraig: при отказе от ЭЛТ существуют более серьезные ограничения. Наиболее заметными из них являются: отсутствие поддержки 64-битных целых чисел, отсутствие поддержки с плавающей запятой и, самое главное, отсутствие обработки исключений. Это на самом деле означает отсутствие обработки исключений - вообще . Даже за исключением SEH. Это особенно сложно компенсировать, и шансы называть CreateThreadсебя Правым становятся все меньше и меньше.
Inspectable
@MichaelBurr: Возможно, вы захотите обновить свой ответ для VC ++ 2015 .
user541686
@Mehrdad: Какие именно изменения вы считаете достойными упоминания?
Inspectable
Я обнаружил, что DisableThreadLibraryCalls не влияет на потоки, созданные с помощью CreateThread, но отключает потоки, созданные с помощью _beginthread или _beginthreadex.
SPlatten
23

В общем, правильнее всего позвонить _beginthread()/_endthread()(или ex()варианты). Однако, если вы используете CRT как .dll, состояние CRT будет правильно инициализировано и уничтожено, поскольку CRT DllMainбудет вызываться с DLL_THREAD_ATTACHи DLL_THREAD_DETACHпри вызове CreateThread()и / ExitThread()или возврате соответственно.

DllMainКод CRT можно найти в каталоге установки для VS под VC \ элт \ SRC \ crtlib.c.

MSN
источник
Отличная отправная точка. После небольшой отладки можно показать, что __CRTDLL_INIT вызывается даже для статически связанной CRT. Стек вызовов init вызывается из _LdrpCallInitRoutine @ 16 (), я не уверен, каким именно механизмом. Это означает, что в последней версии CRT вся инициализация / деинициализация выполняется правильно, за исключением обработки сигналов, которая по-прежнему выполняется вспомогательной функцией _threadstartex, вызываемой из beginthread, но не из CreateThread. Может быть, вы могли бы добавить это к ответу, и я вручу награду?
Suma
Награда назначена, так как это кажется наиболее полезным. Тем не менее, ответ, возможно, стоит обновить. Если вы не можете этого сделать, я могу вернуться к нему через несколько дней.
Suma
1
@MSN: имейте в виду, что CreateThread по-прежнему плохо работает в DLL, если вы снова связываете статический CRT и вызываете DisableThreadLibraryCalls, который отключает вызовы для DLL_THREAD_DETACH. Тогда вы получите утечку памяти. Это описано здесь, в моей статье в базе
знаний
17

Это код, лежащий в основе _beginthreadex(см. crt\src\threadex.c):

    /*
     * Create the new thread using the parameters supplied by the caller.
     */
    if ( (thdl = (uintptr_t)
          CreateThread( (LPSECURITY_ATTRIBUTES)security,
                        stacksize,
                        _threadstartex,
                        (LPVOID)ptd,
                        createflag,
                        (LPDWORD)thrdaddr))
         == (uintptr_t)0 )
    {
            err = GetLastError();
            goto error_return;
    }

Остальная часть _beginthreadexинициализирует структуру данных для каждого потока для CRT.

Преимущество использования _beginthread*заключается в том, что ваши CRT-вызовы из потока будут работать правильно.

Constantin
источник
12

Вы должны использовать _beginthreadили _beginthreadexразрешить библиотеке времени выполнения C выполнять собственную инициализацию потока. Это необходимо знать только программистам на C / C ++, так как теперь они должны знать правила использования собственной среды разработки.

Если вы используете, _beginthreadвам не нужно звонить, так CloseHandleкак RTL сделает за вас. Вот почему вы не можете ждать ручки, если вы использовали _beginthread. Также _beginthreadприводит к путанице, если функция потока завершается немедленно (быстро), поскольку запускающий поток может удерживать недопустимый дескриптор потока для только что запущенного потока.

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

Лучше всего использовать _beginthreadex, запуск приостановлен, затем возобновить после дескриптора записи, ожидание дескриптора в порядке, CloseHandleнеобходимо вызвать.

jarcher7
источник
8

CreateThread()использовались для утечки памяти при использовании любых функций CRT в вашем коде. _beginthreadex()имеет такие же параметры CreateThread()и более универсален, чем _beginthread(). Так что рекомендую использовать _beginthreadex().

неосторожный пешеход
источник
2
Статья 1999 года, возможно, с тех пор была исправлена
bobobobo
1
Эта статья 2005 года все еще подтверждает, что проблема существует.
Jaywalker
2
Да, это применимо только к MSVC ++ 6.0 Service Pack 5 и ранее. (см. раскрывающийся список «Относится к»). Сегодня это не проблема, если вы используете VC7 или выше.
bobobobo 03
1
Это все еще проблема, если вы снова свяжете статический CRT! Также проблема остается, если вы вызываете DisableThreadLibraryCalls в статически связанной библиотеке DLL; см. мою статью в базе
знаний
2
Вы исказили информацию: CreateThreadэто никогда не происходит утечка памяти. Это скорее CRT, когда вызывается из потока, который не был должным образом инициализирован.
Inspectable
6

Относительно вашего обновленного вопроса: «Я также читал в нескольких местах, которые я не могу назвать, WaitForSingleObject()если бы использовал _beginthread(), но если я вызываю _endthread()в потоке, это не должно работать?»

В общем, вы можете передать дескриптор потока WaitForSingleObject()(или другим API, ожидающим дескрипторов объекта) для блокировки, пока поток не завершится. Но дескриптор потока, созданный с помощью _beginthread(), закрывается при _endthread()вызове (что может быть сделано явно или неявно во время выполнения, когда процедура потока возвращается).

Проблема описана в документации для WaitForSingleObject():

Если этот дескриптор закрыт, в то время как ожидание все еще ожидает, поведение функции не определено.

Майкл Берр
источник
5

Глядя на сигнатуры функций, CreateThreadпочти идентично _beginthreadex.

_beginthread,_beginthreadx противCreateThread

HANDLE WINAPI CreateThread(
  __in_opt   LPSECURITY_ATTRIBUTES lpThreadAttributes,
  __in       SIZE_T dwStackSize,
  __in       LPTHREAD_START_ROUTINE lpStartAddress,
  __in_opt   LPVOID lpParameter,
  __in       DWORD dwCreationFlags,
  __out_opt  LPDWORD lpThreadId
);

uintptr_t _beginthread( 
   void( *start_address )( void * ),
   unsigned stack_size,
   void *arglist 
);

uintptr_t _beginthreadex( 
   void *security,
   unsigned stack_size,
   unsigned ( *start_address )( void * ),
   void *arglist,
   unsigned initflag,
   unsigned *thrdaddr 
);

Замечания по здесь говорят , _beginthreadможно использовать либо __cdeclили __clrcallсоглашение о вызове в качестве начальной точки, и _beginthreadexмогут использовать либо __stdcallили __clrcallдля начальной точки.

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

Интересно, что обе _beginthread*функции на самом деле вызываются CreateThreadизнутри C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\crt\srcна моей машине.

// From ~line 180 of beginthreadex.c
/*
 * Create the new thread using the parameters supplied by the caller.
 */
if ( (thdl = (uintptr_t)
      CreateThread( (LPSECURITY_ATTRIBUTES)security,
                    stacksize,
                    _threadstartex,
                    (LPVOID)ptd,
                    createflag,
                    (LPDWORD)thrdaddr))
         == (uintptr_t)0 )
{
        err = GetLastError();
        goto error_return;
}
bobobobo
источник
2
Прокомментируйте, почему вы не должны вызывать CreateThread и смешивать вызовы CRT в этом потоке (определенно не десятилетней давности, и определенно не следует игнорировать) : «Если поток, созданный с помощью CreateThread, вызывает CRT, CRT может завершить процесс в условия нехватки памяти ".
IInspectable
3

beginthreadexдает вам ветку HANDLEдля использования WaitForSingleObjectи друзей. beginthreadне делает. Не забудьте, CloseHandle()когда закончите. Настоящий ответ - использовать boost::threadили скоро использовать класс потоков C ++ 09.

MD XF
источник
В описании msdn сказано, что «В случае успеха каждая из этих функций возвращает дескриптор вновь созданного потока»; ссылаясь на _beginthread () и _beginthreadex () ...
Кирилл
@Kiril: но затем в документации говорится, что _beginthread закрывает для вас дескриптор, а это означает, что вы не можете использовать его, если поток завершается быстро ...
Роджер Липскомб
2

По сравнению с _beginthread, с _beginthreadexвы можете:

  1. Укажите атрибуты безопасности.
  2. Запустить поток в приостановленном состоянии.
  3. Вы можете получить идентификатор потока, который можно использовать с OpenThread.
  4. Возвращенный дескриптор потока гарантированно действителен, если вызов был успешным. Там вам нужно закрыть ручку CloseHandle.
  5. Возвращенный дескриптор потока можно использовать с API синхронизации.

Он _beginthreadexочень похож CreateThread, но первый является реализацией CRT, а второй - вызовом Windows API. В документации по CreateThread есть следующие рекомендации:

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

Вишал
источник
Согласно спецификации API, пункты 3-5 не уникальны _beginthreadex. Вы можете преобразовать результат uintptr_tвозврата из обеих функций в HANDLE.
Andon M. Coleman
Да, теоретически вы правы. На практике разница в том, что _beginthreadручка закрывается при выходе. Таким образом, вы не можете надежно использовать дескриптор с API синхронизации или получить идентификатор потока до тех пор, пока не используете другой способ синхронизации и дублирования дескриптора. Но тогда это _beginthreadexделается за вас.
Vishal
2

CreateThread()когда-то было нет-нет, потому что CRT неправильно инициализировал / очищал. Но это уже история: теперь можно (используя VS2010 и, вероятно, несколько версий назад) вызывать, CreateThread()не нарушая CRT.

Вот официальное подтверждение от MS . В нем указано одно исключение:

Фактически, единственная функция, которую не следует использовать в потоке, созданном с помощью, CreateThread()- это signal()функция.

Однако с точки зрения консистенции я лично предпочитаю продолжать использовать _beginthreadex().

Серж Вотье
источник
Хотя я полагаю, что это правда, можете ли вы предоставить какие-либо авторитетные доказательства - либо путем ссылки на документацию MS, либо путем анализа источников CRT _beginthreadex / _endthreadex?
Suma
@Suma, я думаю, я добавил ссылку MS, пока вы
печатали
Документ, на который вы ссылаетесь, похоже, не подтверждает: «Однако, в зависимости от того, какие функции CRT вызываются, при завершении потоков может быть небольшая утечка памяти». Это означает, что это больше не является большим и общим запретом, но все же запретом, если вы часто создаете потоки и используете в них эти функции. Однако документ датирован 2005 годом и поэтому не может отражать недавнее состояние вопроса.
Suma
1
Хм ... хотя это может зависеть от варианта использования, функция, оставляющая утечку памяти, независимо от размера, я бы рассмотрел нет-нет ... - в частности, если есть альтернатива без утечки!
alk
«Теперь можно вызвать CreateThread (), не нарушая CRT». - К сожалению, это неправда, и никогда не было. Из CreateThread : «Поток в исполняемом файле, который вызывает библиотеку времени выполнения C (CRT), должен использовать функции _beginthreadex и _endthreadex для управления потоками [...] Если поток, созданный с помощью CreateThread, вызывает CRT, CRT может завершить процесс в условиях нехватки памяти ".
IInspectable
2

CreateThread()это вызов Windows API, не зависящий от языка. Он просто создает объект ОС - поток и возвращает HANDLE этому потоку. Все приложения Windows используют этот вызов для создания потоков. Все языки избегают прямого вызова API по очевидным причинам: 1. Вы не хотите, чтобы ваш код был специфичным для ОС 2. Вам нужно немного поработать перед вызовом, подобным API: преобразовать параметры и результаты, выделить временное хранилище и т. Д.

_beginthreadex()- это оболочка C, CreateThread()которая учитывает специфичность C. Он позволяет исходным однопоточным C f-ns работать в многопоточной среде, выделяя хранилище для конкретных потоков.

Если вы не используете CRT, вам не избежать прямого звонка CreateThread(). Если вы используете CRT, вы должны использовать, _beginthreadex()иначе некоторые строки f-n CRT могут не работать должным образом до VC2005.

SKV
источник
1

CreateThread()это прямой системный вызов. Он реализован, на Kernel32.dllкотором, скорее всего, ваше приложение уже будет связано по другим причинам. Он всегда доступен в современных системах Windows.

_beginthread()и _beginthreadex()являются функциями оболочки в Microsoft C Runtime ( msvcrt.dll). Различия между двумя вызовами указаны в документации. Таким образом, он доступен, когда доступна среда выполнения Microsoft C или если ваше приложение статически связано с ней. Скорее всего, вы также будете связываться с этой библиотекой, если только вы не кодируете чистый Windows API (как я лично часто это делаю).

Ваш вопрос логичный и часто повторяющийся. Как и многие другие API, в Windows API есть повторяющиеся и неоднозначные функции, с которыми нам приходится иметь дело. Хуже всего то, что документация не проясняет проблему. Я полагаю, что _beginthread()семейство функций было создано для лучшей интеграции с другими стандартными функциями C, такими как манипуляции с errno. _beginthread()таким образом лучше интегрируется со средой выполнения C.

Несмотря на это, если у вас нет веских причин для использования _beginthread()или _beginthreadex(), вам следует использовать CreateThread(), в основном потому, что вы можете получить на одну библиотечную зависимость меньше в вашем конечном исполняемом файле (а для MS CRT это имеет немного значение). У вас также нет кода оболочки вокруг вызова, хотя этот эффект незначителен. Другими словами, я считаю, что основная причина, по которой CreateThread()стоит придерживаться этого правила, заключается в том, что _beginthreadex()для начала нет веской причины . Функциональные возможности в точности или почти одинаковы.

Одной из веских причин для использования _beginthread() было бы (что кажется ложным), что объекты C ++ будут должным образом развернуты / уничтожены, если они _endthread()были вызваны.

alecov
источник
Там нет неоднозначных вызовов функций на всех . CreateThread- это вызов Windows API для создания потока. Если вы используете CRT (потому что вы программируете на C или C ++), вам следует создавать потоки, используя _beginthread[ex]вызовы CRT (которые вызывают CreateThreadв дополнение к выполнению необходимой инициализации CRT). Наиболее важное различие между _beginthreadи бывшим вариантом: первый сохраняет право собственности на дескриптор собственного потока, а второй передает право собственности вызывающему.
Inspectable
Nitpick: msvcrt.dllэто не библиотека времени выполнения C! См. Blogs.msdn.microsoft.com/oldnewthing/20140411-00/?p=1273
Govind
0

В других ответах не обсуждаются последствия вызова функции времени выполнения C, которая обертывает функцию Win32 API. Это важно при рассмотрении поведения блокировки загрузчика DLL.

Независимо от того, _beginthread{ex}выполняет ли какое-либо специальное управление памятью потоков / волокон во время выполнения C, как обсуждают другие ответы, оно реализовано (при условии динамического связывания со средой выполнения C) DLL, процессы, возможно, еще не загружены.

Это не безопасно звонить _beginthread*из DllMain. Я проверил это, написав DLL, загруженную с помощью функции Windows «AppInit_DLLs». Вызов _beginthreadex (...)вместо CreateThread (...)приводит к тому, что МНОГО важных частей Windows перестает работать во время загрузки, поскольку DllMainточка входа блокируется в ожидании снятия блокировки загрузчика для выполнения определенных задач инициализации.

Кстати, именно поэтому kernel32.dll имеет много перекрывающихся строковых функций, которые выполняет также среда выполнения C - используйте их, DllMainчтобы избежать подобных ситуаций.

Андон М. Коулман
источник
0

Если вы читали книгу «Отладка приложений Windows» от Джеффри Рихтера, в ней он объясняет, что почти во всех случаях вы должны вызывать, _beginthreadexа не вызывать CreateThread. _beginthreadэто просто упрощенная оболочка _beginthreadex.

_beginthreadexинициализирует определенные внутренние компоненты CRT (C RunTime), которые CreateThreadAPI не выполняет.

Последствие использования CreateThreadAPI вместо использования _begingthreadexвызовов функций CRT может привести к неожиданным проблемам.

Ознакомьтесь с этим старым журналом Microsoft Journal от Рихтера.

Эхсан Самани
источник
-2

Между ними больше нет разницы.

Все комментарии об утечках памяти и т. Д. Основаны на очень старых версиях <VS2005. Я провел несколько стресс-тестов много лет назад и смог развенчать этот миф. Даже Microsoft смешивает стили в своих примерах, почти никогда не используя _beginthread.

Лотар
источник
CreateThread : «Если поток, созданный с помощью CreateThread, вызывает CRT, CRT может завершить процесс в условиях нехватки памяти».
IInspectable
На основании предложения «требует использования многопоточной версии CRT» я предполагаю, что это мусор документации, поскольку многопоточной версии crt уже много лет нет.
Lothar
«больше нет многопоточной версии CRT » - MSDN утверждает, что «[т] однопоточная CRT больше не доступна». Вы не можете быть правы обоими. Я тоже пойду с MSDN.
Inspectable
Это была опечатка, конечно, я имел в виду, что однопоточный больше не используется, а многопоточный стал стандартом, и исчезла разница между использованием или неиспользованием потоков.
Lothar
Это действительно становится странным. Теперь вы используете оператор, который, несомненно, правильный ( «требует использования многопоточной версии CRT» ), чтобы утверждать, что и этот оператор, и остальная часть документации, скорее всего, неверны? Это точно звучит неправильно.
IInspectable