Given shared_ptr variable declared in condition clause of for loop and for loop body contains if/continue statement, microsoft compiler (as of version 2015) generates extra destructor call (two total) per loop iteration. This causes destruction of Item objects that should be out of reach of Holder interface users. See example code below
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();
}
This yield output below
|| [ 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)
If I move _item declaration out of for loop this way
auto _item = ItemCPtr();
for (size_t i = 0; _item = _holder.getItem(i); ++i)
then I get expected output like this
|| [ 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)
As far as I can understand, getItem should yield a copy of ItemCPtr and no Items could be modified through Holder interface. Yet user can destroy two out of five items inside loop, see ~Item(1)
and ~Item(3)
destructor output between before loop
/after loop
marks.
This is trivial example to expose problem. In real world this would cause hard to track memory corruption problem.
Compiler identity:
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 is 64bit Windows 10
Bug manifests itself even when optimization is disabled /Od
, default options.
Microsoft Visual Studio is a good compiler for developing Windows applications. Although Visual Studio presents a ton of choices to the user when first starting out (for instance, there are a lot of different project types), the amount of choice gives a good idea of the overall scope of this tool.
Microsoft C++ Compiler (MSVC) This is the default compiler for most Visual Studio C++ projects and is recommended if you are targeting Windows.
MSVC is doing the compilation job significantly faster than MinGW-w64. The DLL sizes are comparable, if optimization is set to "-O2" for MinGW-w64, with "-O3" the DLLs from MinGW-w64 are larger. Binary files compiled with MinGW-w64 are performing significantly better than those compiled with MSVC.
The /std:c++17 option enables C++17 standard-specific features and behavior. It enables the full set of C++17 features implemented by the MSVC compiler. This option disables compiler and standard library support for features that are new or changed after C++17.
However, when Clang and GCC accept your code without a diagnostic, and MSVC doesn't, you've probably found a bug in our compiler. (Other possibilities include differences in Unix and Windows behavior, or different levels of C++ standards implementation, and so on.)
However, in theory this bug—and more generally, bugs just like it—could lead to an improper optimization in your code. That is among the worst sort of bug a compiler can have: silent-bad-codegen. I’m very pleased to have squashed this one.
If you find problems in the Microsoft C++ compiler (MSVC), the linker, or other tools and libraries, we want to know about them. When the issue is in our documentation, we want to know about that, too. The best way to let us know about a problem is to send us a report that includes a description of the problem you've discovered.
(Other possibilities include differences in Unix and Windows behavior, or different levels of C++ standards implementation, and so on.) When all the compilers reject your code, then it's likely that your code is incorrect. Seeing different error messages may help you diagnose the issue yourself.
As far as I can understand, getItem should yield a copy of ItemCPtr and no Items could be modified through Holder interface.
Your understanding is 100% accurate. That is exactly the behavior described for the C++ standard's abstract machine.
Even under the as-if rule, which allows compilers to optimize, the observable behavior (on account of printing to the standard output stream) should be as though there was at least one shared pointer (for each item) that exists until the end of the test's scope. That is clearly not what you see though.
My educated guess would be that MSVC optimizes the copies out entirely. And instead refers to pointers inside the vector directly in the loop body. That on its own is okay.
The bug would likely be that it mishandles the code which would have destroyed the local variable in case of a continue
statment. And errounosly applies it to the object in the vector. This is a bug.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With