2

Given the following test code:

void Serialize(std::ostream& os, int& i)
{
    os << "int: " << i << '\n';
}

template<typename T>
void Write(std::ostream& os, T* pData)
{
    ::Serialize(os, *pData);
}

struct SData
{
    int i = 42;
};

void Serialize(std::ostream& os, SData& rData)
{
    os << "SData: " << rData.i << '\n';
}

void Test()
{
    auto os = std::ofstream("c:/test.out");
    auto s = SData();
    Write(os, &s);
}

int main([[maybe_unused]] int, [[maybe_unused]] char*[])
{
    Test();
}

The MSVC compiler with /permissive- (compiler set to standards conformance mode) fails to compile this as only the Serialize free function(s) declared prior to the the function template definition are visible to the function template.

The above compiles fine with the MSVC compiler either using /permissive (standards conformance mode disabled) or removing the global scope operator on the Serialize call.

The above also compiles fine when using the LLVM (clang-cl) toolset in VS 2022, or using clang++.

I would expect that the lookup occurs at the point that the function template is instantiated, and it appears that this is not the case with MSVC using /permissive-. Microsoft replied to my community post that this is not a bug. I'd like to know why this is expected behavior.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
cue
  • 41
  • 4
  • 4
    This has been explained by msvc team in their blogpost. https://devblogs.microsoft.com/cppblog/two-phase-name-lookup-support-comes-to-msvc/ – Osyotr Mar 15 '23 at 20:09
  • A very informative link. Thanks, that MS devblog post has an almost identical code example to what I cobbled together. – cue Mar 16 '23 at 08:23

1 Answers1

1

Normal qualified and unqualified name lookup is done from the point-of-definition of the template. Only argument-dependent lookup (ADL) is done from the point-of-instantiation of the template specialization (if the call is dependent).

You are using a qualified call ::Serialize(os, *pData); and qualified calls suppress ADL. Therefore it will never consider Serialize overloads declared later.

If you remove :: then the call becomes unqualified and ADL will be done from the point-of-instantiation, which recursively from the implicit instantiation from Write(os, &s); in Test are two possible points in this case: One directly after the definition of Test and one after the the end of the translation unit. In either case because SData is an argument type to the call and because the void Serialize(std::ostream& os, SData& rData) overload is in the same namespace scope as SData, declared before the point-of-instantiation, it will be found by ADL.

user17732522
  • 53,019
  • 2
  • 56
  • 105