VS2022与CompilerExplorer同版本MSVC编译自定义Allocator异常问题
std::_Container_proxy实例化失败,其他编译器正常 我实现了一个兼容STL的Allocator<T>,用于基于Tag跟踪内存分配,要求T必须拥有static constexpr成员tag。但在VS2022中编译时,模板实例化过程中T被替换为不具备tag成员的std::_Container_proxy,导致编译失败。奇怪的是,同版本MSVC的CompilerExplorer以及GCC 10+都能正常编译。
环境信息
- CompilerExplorer编译器:x64 msvc 19.33
- VS2022的
_MSC_VER:1933 - C++版本:20
相关代码
#include <vector> #include <iostream> namespace Memory { enum class Tag { None = 0, User = 1, }; void* allocate(const std::size_t size, const Tag tag) { std::cout << "Allocate " << size << " bytes (tag: " << (int) tag << ")\n"; return ::operator new(size); } void deallocate(void* block, const std::size_t size, const Tag tag) { std::cout << "Deallocate " << size << " bytes (tag: " << (int) tag << ")\n"; ::operator delete(block, size); } template <typename T> class Allocator { public: using value_type = T; Allocator() = default; template <typename U> Allocator(const Allocator<U>&) noexcept {} template <typename U> bool operator==(const Allocator<U>&) const noexcept { return true; } template <typename U> bool operator!=(const Allocator<U>&) const noexcept { return false; } T* allocate(const std::size_t count) const { return (T*) Memory::allocate(sizeof(T) * count, T::tag); } void deallocate(void* ptr, const std::size_t count) const noexcept { Memory::deallocate(ptr, sizeof(T) * count, T::tag); } }; } struct NoneTest { static constexpr auto tag { Memory::Tag::None }; }; struct UserTest { static constexpr auto tag { Memory::Tag::User }; }; int main() { std::vector<NoneTest, Memory::Allocator<NoneTest>> noneVector; std::vector<UserTest, Memory::Allocator<UserTest>> userVector; noneVector.reserve(42); userVector.reserve(73); return 0; }
预期输出
Allocate 42 bytes (tag: 0) Allocate 73 bytes (tag: 1) Deallocate 73 bytes (tag: 1) Deallocate 42 bytes (tag: 0)
错误信息
1>C:\dev\test\test\main.cpp(37,63): error C2039: 'tag': is not a member of 'std::_Container_proxy' 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\xmemory(1044,8): message : see declaration of 'std::_Container_proxy' 1>C:\dev\test\test\main.cpp(36,1): message : while compiling class template member function 'T *Memory::Allocator<T>::allocate(const size_t) const' 1> with 1> [ 1> T=std::_Container_proxy 1> ] 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\xmemory(1064,44): message : see reference to function template instantiation 'T *Memory::Allocator<T>::allocate(const size_t) const' being compiled 1> with 1> [ 1> T=std::_Container_proxy 1> ] 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\vector(832): message : see reference to class template instantiation 'Memory::Allocator<_Newfirst>' being compiled 1> with 1> [ 1> _Newfirst=std::_Container_proxy 1> ] 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\vector(829,1): message : while compiling class template member function 'std::vector<UserTest,Memory::Allocator<UserTest>>::~vector(void) noexcept' 1>C:\dev\test\test\main.cpp(56,56): message : see reference to function template instantiation 'std::vector<UserTest,Memory::Allocator<UserTest>>::~vector(void) noexcept' being compiled 1>C:\dev\test\test\main.cpp(56,56): message : see reference to class template instantiation 'std::vector<UserTest,Memory::Allocator<UserTest>>' being compiled 1>C:\dev\test\test\main.cpp(37,1): error C2065: 'tag': undeclared identifier
原因分析
VS的STL实现中,容器(如std::vector)会使用内部类型std::_Container_proxy管理迭代器调试等状态,并且会尝试用用户提供的分配器分配该对象。你的分配器强制要求T拥有tag成员,而std::_Container_proxy不满足此条件,导致实例化Allocator<std::_Container_proxy>时触发编译错误。
GCC和CompilerExplorer的MSVC能正常编译,是因为它们的STL实现要么未使用用户分配器管理_Container_proxy,要么采用了延迟实例化逻辑——仅当真正调用allocate方法时才检查T::tag,而实际容器并未为_Container_proxy调用你的分配器方法。
解决方案
方案1:为std::_Container_proxy特化分配器
直接为VS STL的内部类型std::_Container_proxy提供特化版本的分配器,指定默认Tag或使用标准分配逻辑:
namespace Memory { template <> class Allocator<std::_Container_proxy> { public: using value_type = std::_Container_proxy; Allocator() = default; template <typename U> Allocator(const Allocator<U>&) noexcept {} template <typename U> bool operator==(const Allocator<U>&) const noexcept { return true; } template <typename U> bool operator!=(const Allocator<U>&) const noexcept { return false; } std::_Container_proxy* allocate(const std::size_t count) const { return (std::_Container_proxy*) Memory::allocate(sizeof(std::_Container_proxy) * count, Tag::None); } void deallocate(void* ptr, const std::size_t count) const noexcept { Memory::deallocate(ptr, sizeof(std::_Container_proxy) * count, Tag::None); } }; }
注意:该方案依赖VS STL的内部实现,版本更新可能导致失效,跨平台性差。
方案2:用SFINAE约束分配逻辑
通过SFINAE(替换失败不是错误),仅当T拥有tag成员时启用自定义分配,否则回退到默认逻辑:
#include <type_traits> namespace Memory { enum class Tag { None = 0, User = 1, }; void* allocate(const std::size_t size, const Tag tag) { std::cout << "Allocate " << size << " bytes (tag: " << (int) tag << ")\n"; return ::operator new(size); } void deallocate(void* block, const std::size_t size, const Tag tag) { std::cout << "Deallocate " << size << " bytes (tag: " << (int) tag << ")\n"; ::operator delete(block, size); } // 检测T是否拥有static constexpr tag成员 template <typename T, typename = void> struct has_tag : std::false_type {}; template <typename T> struct has_tag<T, std::void_t<decltype(T::tag)>> : std::true_type {}; template <typename T> constexpr bool has_tag_v = has_tag<T>::value; template <typename T> class Allocator { public: using value_type = T; Allocator() = default; template <typename U> Allocator(const Allocator<U>&) noexcept {} template <typename U> bool operator==(const Allocator<U>&) const noexcept { return true; } template <typename U> bool operator!=(const Allocator<U>&) const noexcept { return false; } // 有tag成员时使用自定义分配 template <typename U = T> std::enable_if_t<has_tag_v<U>, U*> allocate(const std::size_t count) const { return (U*) Memory::allocate(sizeof(U) * count, U::tag); } // 无tag成员时回退到默认分配 template <typename U = T> std::enable_if_t<!has_tag_v<U>, U*> allocate(const std::size_t count) const { return (U*) ::operator new(sizeof(U) * count); } // 同理处理deallocate template <typename U = T> std::enable_if_t<has_tag_v<U>, void> deallocate(void* ptr, const std::size_t count) const noexcept { Memory::deallocate(ptr, sizeof(U) * count, U::tag); } template <typename U = T> std::enable_if_t<!has_tag_v<U>, void> deallocate(void* ptr, const std::size_t count) const noexcept { ::operator delete(ptr, sizeof(U) * count); } }; }
该方案通用且跨平台,不依赖具体STL实现。
方案3:让分配器携带Tag参数(最健壮)
将Tag作为分配器的模板参数,而非从T中获取,彻底避免依赖容器内部类型:
namespace Memory { enum class Tag { None = 0, User = 1, }; void* allocate(const std::size_t size, const Tag tag) { std::cout << "Allocate " << size << " bytes (tag: " << (int) tag << ")\n"; return ::operator new(size); } void deallocate(void* block, const std::size_t size, const Tag tag) { std::cout << "Deallocate " << size << " bytes (tag: " << (int) tag << ")\n"; ::operator delete(block, size); } template <typename T, Tag TagValue = Tag::None> class Allocator { public: using value_type = T; static constexpr Tag tag = TagValue; Allocator() = default; template <typename U, Tag OtherTag> Allocator(const Allocator<U, OtherTag>&) noexcept {} template <typename U, Tag OtherTag> bool operator==(const Allocator<U, OtherTag>&) const noexcept { return true; } template <typename U, Tag OtherTag> bool operator!=(const Allocator<U, OtherTag>&) const noexcept { return false; } T* allocate(const std::size_t count) const { return (T*) Memory::allocate(sizeof(T) * count, tag); } void deallocate(void* ptr, const std::size_t count) const noexcept { Memory::deallocate(ptr, sizeof(T) * count, tag); } }; } // 使用方式 int main() { std::vector<NoneTest, Memory::Allocator<NoneTest, Memory::Tag::None>> noneVector; std::vector<UserTest, Memory::Allocator<UserTest, Memory::Tag::User>> userVector; noneVector.reserve(42); userVector.reserve(73); return 0; }
该方案符合STL分配器的设计习惯,完全不受容器内部类型影响,是最健壮的解决方案。
内容的提问来源于stack exchange,提问作者Mateo

