Condividi tramite


Override AtlThrow with care

ATL allows you to replace its stock routine, AtlThrow, for communicating an error back to the caller using exceptions. This is documented on MSDN here: https://msdn.microsoft.com/en-us/library/z325eyx0.aspx.

Most of the time, when ATL encounters an error and you overrode AtlThrow with a custom implementation, the latter will be called (as expected). However, a customer reported that under certain circumstances, the stock implementation will be called, no matter what other implementation you supplied.

While this issue is certainly caused by ATL’s current design, it’s very interesting to look behind the curtains.

How ATL implements this

ATL is shipped with source code, so we can easily check how it’s done.

Essentially, AtlThrow is a preprocessor symbol, defined in atldef.h:

#define AtlThrow ATL::AtlThrowImpl

If you define _ATL_CUSTOM_THROW and your own AtlThrow, then this stock definition will not be processed thanks to an #ifdef.
In ATL code, when an error is detected, AtlThow is “called” instead of ATL::AtlThrowImpl. That way, if you define both _ATL_CUSTOM_THROW and AtlThrow, the pre-processor resolves AtlThrow to your symbol, and thus your function gets called.

This solution has an important requirement though. Since the pre-processor runs only at compile-time, your custom implementation will be used only if all callers are recompiled. How does ATL fulfill this?

1. ATL source comprises many .h, .inl and .cpp files. When you add support for ATL to your project, you essentially include the core set of ATL header files and implicitly link to the static or the stub library (the latter is needed to use ATL in DLL). Since many ATL functions are actually implemented inline in the header files, you actually compile these functions every time you build your applications (of course, performance will not suffer due to precompiled headers).

2. AtlThrow is called solely in .h and .inl files. It is not called in .cpp files that make up ATL.

Therefore, the compiler will always produce a variant of the functions calling AtlThrow in the header files that calls your implementation. When calling functions defined originally in ATL .cpp files (which reside in the static library or dynamic DLL), which in turn call functions that may call AtlThrow, you still get the right behavior because the linker chooses the variant calling your implementation.

However, this assumption turns out to be false.

Background

As Mike Marcelais, a colleague here at Microsoft explained to me, C++ has a rule called the ‘One Definition Rule’ (ODR). This basically says that even though you can declare something as many times as you want, you can only define it once. Of course, this applies to functions as well.

Inline functions, to be useful, need to go in header files so that everyone can see them. Since the compiler can’t guarantee that it will always inline an inline function, the compiler also generates a non-inline version of each inline function. Since the function is defined in a header file, each .cpp file will have its own copy of the function.

Since that basically violates the ODR to have multiple definitions of the inline function, the C++ standard created a special case for inline functions: An inline function is allowed to be defined multiple times, as long as they are always the same. Since it is almost impossible for compilers to figure out when two functions are “the same”, compilers are not required to report errors in this case. Note that this is generally true for ODR violations – detecting them can be quite hard (or even impossible) so the standard generally doesn’t require that the compiler tools detect them and report them as errors.)

In reality, if you have two different versions of the same inline function, then the linker will see both implementations when it creates the final binary. Normally, it would report an error (“multiply defined symbol”) but it notices that both functions were marked as inline, and therefore it assumes that they must be copies of the same function, and it picks one “at random” to use and throws the other copies away. This randomness could be a function of a few factors, e.g. switching from a debug to a release build. If the copies really were the same, it wouldn’t matter which one the linker uses. The result is that sometimes version A is used, but when you recompile, version B might be used.

Of course, even if version A was selected by the linker, any places that could see version B and where the compiler decided to inline version B in the caller would still be using version B.

You can provoke the unexpected behavior with the following code. Note that with Visual C++ 2010, debug build of this produces the unexpected behavior described above (ATL::AtlThrowImpl will be called), while the release build yields the intended one (MyThrowImpl will execute).

#define _ATL_CUSTOM_THROW
#define AtlThrow MyThrowImpl
void __stdcall MyThrowImpl(int i);
#include <stdio.h>
#include <tchar.h>
#include <atlbase.h>
#include <atlstr.h>
#include <atltime.h>

int _tmain(int argc, _TCHAR* argv[])
{
       CTime time(11111, 11, 10, 10, 10, 10);
       return 0;
}

void __stdcall MyThrowImpl(int i)
{
       printf("MyThrowImpl");
}

Resolution

The ATL team is aware of this issue, so the design may be changed in the future (note that there’s no guarantee for this).

We’ll definitely fix the documentation on MSDN, and in the meantime, you can get around this this by re-building ATL with your error handler. See readme.txt in C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\atlmfc\src\ for instructions on building ATL.