可能的 MSVC 编译器错误
Possible MSVC compiler bug
鉴于在 for 循环的条件子句中声明的 shared_ptr 变量和 for 循环主体包含 if/continue 语句,Microsoft 编译器(从 2015 版开始)在每次循环迭代时生成额外的析构函数调用(总共两次) .这会导致 Holder 接口用户无法访问的 Item 对象的破坏。
请参阅下面的示例代码
namespace
{
class Item
{
public:
Item(size_t v)
: value_(v)
{
std::cout << "Item(" << value_ << ")" << std::endl;
}
~Item()
{
std::cout << "~Item(" << value_ << ")" << std::endl;
}
void print() const
{
std::cout << "Item::print(" << value_ << ")" << std::endl;
}
private:
size_t value_;
};
typedef std::shared_ptr<const Item> ItemCPtr;
class Holder
{
public:
Holder(size_t n)
{
for (size_t i = 0; i < n; ++i)
items_.emplace_back(new Item(i));
}
ItemCPtr getItem(size_t i) const
{
if (i < items_.size())
return items_[i];
return ItemCPtr();
}
private:
std::vector<ItemCPtr> items_;
};
}
TEST(Test, Test)
{
Holder _holder(5);
std::cout << "before loop" << std::endl;
for (size_t i = 0; auto _item = _holder.getItem(i); ++i)
{
if (!!(i % 2))
continue;
_item->print();
}
std::cout << "after loop" << std::endl;
_holder.getItem(1)->print();
_holder.getItem(3)->print();
}
下面的产量输出
|| [ RUN ] Test.Test
|| Item(0)
|| Item(1)
|| Item(2)
|| Item(3)
|| Item(4)
|| before loop
|| Item::print(0)
|| ~Item(1)
|| Item::print(2)
|| ~Item(3)
|| Item::print(4)
|| after loop
|| Item::print(3722304989)
|| Item::print(3722304989)
|| ~Item(0)
|| ~Item(2)
|| ~Item(4)
|| [ OK ] Test.Test (0 ms)
如果我以这种方式将 _item 声明移出 for 循环
auto _item = ItemCPtr();
for (size_t i = 0; _item = _holder.getItem(i); ++i)
然后我得到这样的预期输出
|| [ RUN ] Test.Test
|| Item(0)
|| Item(1)
|| Item(2)
|| Item(3)
|| Item(4)
|| before loop
|| Item::print(0)
|| Item::print(2)
|| Item::print(4)
|| after loop
|| Item::print(1)
|| Item::print(3)
|| ~Item(0)
|| ~Item(1)
|| ~Item(2)
|| ~Item(3)
|| ~Item(4)
|| [ OK ] Test.Test (0 ms)
据我了解,getItem 应该生成 ItemCPtr 的副本,并且不能通过 Holder 接口修改任何项目。然而,用户可以在循环内销毁五分之二的项目,请参阅 ~Item(1)
和 ~Item(3)
析构函数在 before loop
/after loop
标记之间的输出。
这是暴露问题的小例子。在现实世界中,这会导致难以跟踪内存损坏问题。
编译器标识:
cmake -G "Visual Studio 14 2015" ..\
-- Selecting Windows SDK version 10.0.14393.0 to target Windows 10.0.19041.
-- The C compiler identification is MSVC 19.0.24210.0
-- The CXX compiler identification is MSVC 19.0.24210.0
OS 是 64 位 Windows 10
即使禁用优化 /Od
,默认选项也会出现错误。
As far as I can understand, getItem should yield a copy of ItemCPtr and no Items could be modified through Holder interface.
您的理解是 100% 准确的。这正是为 C++ 标准的抽象机描述的行为。
即使在允许编译器优化的 as-if 规则下,可观察到的行为(由于打印到标准输出流)也应该好像至少有一个共享指针(对于每个项目)存在直到测试范围结束。这显然不是你看到的。
我有根据的猜测是 MSVC 完全优化了副本。而是直接在循环体中引用向量内部的指针。这本身就可以了。
该错误可能是它错误处理了在 continue
语句的情况下会破坏局部变量的代码。并且错误地将它应用于向量中的对象。这是一个错误。
鉴于在 for 循环的条件子句中声明的 shared_ptr 变量和 for 循环主体包含 if/continue 语句,Microsoft 编译器(从 2015 版开始)在每次循环迭代时生成额外的析构函数调用(总共两次) .这会导致 Holder 接口用户无法访问的 Item 对象的破坏。 请参阅下面的示例代码
namespace
{
class Item
{
public:
Item(size_t v)
: value_(v)
{
std::cout << "Item(" << value_ << ")" << std::endl;
}
~Item()
{
std::cout << "~Item(" << value_ << ")" << std::endl;
}
void print() const
{
std::cout << "Item::print(" << value_ << ")" << std::endl;
}
private:
size_t value_;
};
typedef std::shared_ptr<const Item> ItemCPtr;
class Holder
{
public:
Holder(size_t n)
{
for (size_t i = 0; i < n; ++i)
items_.emplace_back(new Item(i));
}
ItemCPtr getItem(size_t i) const
{
if (i < items_.size())
return items_[i];
return ItemCPtr();
}
private:
std::vector<ItemCPtr> items_;
};
}
TEST(Test, Test)
{
Holder _holder(5);
std::cout << "before loop" << std::endl;
for (size_t i = 0; auto _item = _holder.getItem(i); ++i)
{
if (!!(i % 2))
continue;
_item->print();
}
std::cout << "after loop" << std::endl;
_holder.getItem(1)->print();
_holder.getItem(3)->print();
}
下面的产量输出
|| [ RUN ] Test.Test
|| Item(0)
|| Item(1)
|| Item(2)
|| Item(3)
|| Item(4)
|| before loop
|| Item::print(0)
|| ~Item(1)
|| Item::print(2)
|| ~Item(3)
|| Item::print(4)
|| after loop
|| Item::print(3722304989)
|| Item::print(3722304989)
|| ~Item(0)
|| ~Item(2)
|| ~Item(4)
|| [ OK ] Test.Test (0 ms)
如果我以这种方式将 _item 声明移出 for 循环
auto _item = ItemCPtr();
for (size_t i = 0; _item = _holder.getItem(i); ++i)
然后我得到这样的预期输出
|| [ RUN ] Test.Test
|| Item(0)
|| Item(1)
|| Item(2)
|| Item(3)
|| Item(4)
|| before loop
|| Item::print(0)
|| Item::print(2)
|| Item::print(4)
|| after loop
|| Item::print(1)
|| Item::print(3)
|| ~Item(0)
|| ~Item(1)
|| ~Item(2)
|| ~Item(3)
|| ~Item(4)
|| [ OK ] Test.Test (0 ms)
据我了解,getItem 应该生成 ItemCPtr 的副本,并且不能通过 Holder 接口修改任何项目。然而,用户可以在循环内销毁五分之二的项目,请参阅 ~Item(1)
和 ~Item(3)
析构函数在 before loop
/after loop
标记之间的输出。
这是暴露问题的小例子。在现实世界中,这会导致难以跟踪内存损坏问题。
编译器标识:
cmake -G "Visual Studio 14 2015" ..\
-- Selecting Windows SDK version 10.0.14393.0 to target Windows 10.0.19041.
-- The C compiler identification is MSVC 19.0.24210.0
-- The CXX compiler identification is MSVC 19.0.24210.0
OS 是 64 位 Windows 10
即使禁用优化 /Od
,默认选项也会出现错误。
As far as I can understand, getItem should yield a copy of ItemCPtr and no Items could be modified through Holder interface.
您的理解是 100% 准确的。这正是为 C++ 标准的抽象机描述的行为。
即使在允许编译器优化的 as-if 规则下,可观察到的行为(由于打印到标准输出流)也应该好像至少有一个共享指针(对于每个项目)存在直到测试范围结束。这显然不是你看到的。
我有根据的猜测是 MSVC 完全优化了副本。而是直接在循环体中引用向量内部的指针。这本身就可以了。
该错误可能是它错误处理了在 continue
语句的情况下会破坏局部变量的代码。并且错误地将它应用于向量中的对象。这是一个错误。