提问人:Rob McDonald 提问时间:12/24/2021 最后编辑:Rob McDonald 更新时间:1/5/2022 访问量:183
C++ 容器中的逻辑常量
Logical const in a container in C++
问:
编辑以包括 MWE(删除 example-lite)并添加了有关编译和 Valgrind 输出的详细信息。
我正在使用 mutable 关键字来实现延迟评估和缓存结果的结果。这对于单个对象工作正常,但对于集合似乎不能按预期工作。
我的情况更复杂,但假设我有一个三角形类,可以计算三角形的面积并缓存结果。我在我的例子中使用指针,因为被延迟计算的东西是一个更复杂的类(它实际上是同一类的另一个实例,但我试图简化这个例子)。
我还有另一个类,它本质上是三角形的集合。它有一种方法可以计算所有包含的三角形的总面积。
从逻辑上讲,tri::Area() 是 const -- mesh::Area() 是 const。当如上实现时,Valgrind 显示内存泄漏 (m_Area)。
我相信由于我使用的是const_iterator,因此对 tri::Area() 的调用正在作用于三角形的副本。在该副本上调用 Area(),该副本执行新操作,计算面积并返回结果。此时,副本丢失,内存泄漏。
此外,我相信这意味着该区域实际上没有被缓存。下次我调用 Area() 时,它会泄漏更多内存并再次进行计算。显然,这是不理想的。
一种解决方案是使 mesh::Area() 不为常量。这不是很好,因为它需要从其他常量方法调用。
我认为这可能会起作用(将m_Triangles标记为可变并使用常规迭代器):
但是,我不喜欢将m_Triangles标记为可变的——我更愿意保留编译器在其他不相关的方法中保护m_Triangles一致性的能力。所以,我很想用const_cast将丑陋的东西本地化到需要它的方法。像这样的东西(可能会出错):
不确定如何使用 const_cast 实现 - 我应该铸造m_Triangles还是这个?如果我投射这个,m_Triangles可见吗(因为它是私有的)?
我是否错过了其他方式?
我想要的效果是保持 mesh::Area() 标记为 const,但调用它会导致所有 tris 计算并缓存它们的m_Area。当我们在做这件事时 - 没有内存泄漏,Valgrind很高兴。
我发现了很多在对象中使用可变对象的例子——但没有关于在另一个对象的集合中使用该对象的例子。链接到有关此的博客文章或教程文章会很棒。
感谢您的帮助。
更新
从这个 MWE 来看,我似乎对泄漏点的看法是错误的。
如果删除了对的调用,则下面的代码是 Valgrind-clean。SplitIndx()
此外,我还添加了一个简单的测试,以确认缓存的值是否在容器存储的对象中存储和更新。
现在看来,呼叫是发生泄漏的地方。我应该如何堵住这个泄漏点?m_Triangles[indx] = t1;
#include <cmath>
#include <map>
#include <cstdio>
class point
{
public:
point()
{
v[0] = v[1] = v[2] = 0.0;
}
point( double x, double y, double z )
{
v[0] = x; v[1] = y; v[2] = z;
}
double v[3];
friend point midpt( const point & p1, const point & p2 );
friend double dist( const point & p1, const point & p2 );
friend double area( const point & p1, const point & p2, const point & p3 );
};
point midpt( const point & p1, const point & p2 )
{
point pmid;
pmid.v[0] = 0.5 * ( p1.v[0] + p2.v[0] );
pmid.v[1] = 0.5 * ( p1.v[1] + p2.v[1] );
pmid.v[2] = 0.5 * ( p1.v[2] + p2.v[2] );
return pmid;
}
double dist( const point & p1, const point & p2 )
{
double dx = p2.v[0] - p1.v[0];
double dy = p2.v[1] - p1.v[1];
double dz = p2.v[2] - p1.v[2];
return sqrt( dx * dx + dy * dy + dz * dz );
}
double area( const point & p1, const point & p2, const point & p3 )
{
double a = dist( p1, p2 );
double b = dist( p1, p3 );
double c = dist( p2, p3 );
// Place in increasing order a, b, c.
if ( a < b )
{
std::swap( a, b );
}
if ( a < c )
{
std::swap( a, c );
}
if ( b < c )
{
std::swap( b, c );
}
if ( c-(a-b) < 0.0 )
{
// Not a real triangle.
return 0.0;
}
return 0.25 * sqrt( ( a + ( b + c ) ) * ( c - ( a - b ) ) * ( c + ( a - b ) ) * ( a + ( b - c ) ) );
}
class tri
{
public:
tri()
{
m_Area = NULL;
}
tri( const point & p1, const point & p2, const point & p3 )
{
m_P1 = p1; m_P2 = p2; m_P3 = p3;
m_Area = NULL;
}
~tri() {
delete m_Area;
}
tri( const tri & t )
{
m_P1 = t.m_P1;
m_P2 = t.m_P2;
m_P3 = t.m_P3;
if ( t.m_Area )
{
m_Area = new double( *(t.m_Area) );
}
else
{
m_Area = NULL;
}
}
tri & operator=( const tri & t )
{
if ( this != &t )
{
m_P1 = t.m_P1;
m_P2 = t.m_P2;
m_P3 = t.m_P3;
if ( t.m_Area )
{
m_Area = new double( *(t.m_Area) );
}
else
{
m_Area = NULL;
}
}
return *this;
}
bool KnowsArea() const
{
if ( !m_Area ) return false;
return true;
}
void SetPts( const point & p1, const point & p2, const point & p3 )
{
m_P1 = p1; m_P2 = p2; m_P3 = p3;
delete m_Area;
m_Area = NULL;
}
double Area() const
{
if ( !m_Area )
{
m_Area = new double;
*m_Area = area( m_P1, m_P2, m_P3 );
}
return *m_Area;
}
void Split( tri & t1, tri & t2 )
{
point p4 = midpt( m_P2, m_P3 );
t1.SetPts( m_P1, m_P2, p4 );
t2.SetPts( m_P1, p4, m_P3 );
}
private:
point m_P1;
point m_P2;
point m_P3;
mutable double * m_Area;
};
class mesh
{
public:
double Area() const
{
double area = 0;
std::map<int,tri>::const_iterator it;
for (it=m_Triangles.begin(); it!=m_Triangles.end(); ++it)
{
area += it->second.Area();
}
return area;
}
std::map<int, tri> m_Triangles;
int KnownArea() const
{
int count = 0;
std::map<int,tri>::const_iterator it;
for (it=m_Triangles.begin(); it!=m_Triangles.end(); ++it)
{
if ( it->second.KnowsArea() ) count++;
}
return count;
}
void SplitIndx( int indx )
{
tri t1, t2;
m_Triangles[indx].Split( t1, t2 );
m_Triangles[indx] = t1;
m_Triangles[m_Triangles.size()+1] = t2;
}
int NumTri() const
{
return m_Triangles.size();
}
};
int main( void )
{
point p1( 0, 0, 0 );
point p2( 1, 0, 0 );
point p3( 0, 1, 0 );
point p4( 1, 1, 0 );
point p5( 3, 4, 0 );
tri t1( p1, p2, p3 );
tri t2( p1, p2, p4 );
tri t3( p1, p3, p4 );
tri t4( p1, p3, p5 );
tri t5( p1, p4, p5 );
mesh m;
m.m_Triangles[1] = t1;
m.m_Triangles[2] = t2;
m.m_Triangles[3] = t3;
m.m_Triangles[4] = t4;
m.m_Triangles[5] = t5;
printf( "Known areas before total %d of %d\n", m.KnownArea(), m.NumTri() );
double area = m.Area();
printf( "Total area is %f\n", area );
printf( "Known areas after total %d of %d\n", m.KnownArea(), m.NumTri() );
printf( "Splitting\n" );
m.SplitIndx( 3 );
printf( "Known areas before total %d of %d\n", m.KnownArea(), m.NumTri() );
area = m.Area();
printf( "Total area is %f\n", area );
printf( "Known areas after total %d of %d\n", m.KnownArea(), m.NumTri() );
return 0;
}
编译方式:
clang++ -Wall -std=c++11 -stdlib=libc++ mwe.cpp -o mwe
或:
g++ -Wall -std=c++11 mwe.cpp -o mwe
Valgrind 输出(来自 clang):
$ valgrind --track-origins=yes --leak-check=full ./mwe
==231996== Memcheck, a memory error detector
==231996== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==231996== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==231996== Command: ./mwe
==231996==
Known areas before total 0 of 5
Total area is 3.500000
Known areas after total 5 of 5
Splitting
Known areas before total 4 of 6
Total area is 3.500000
Known areas after total 6 of 6
==231996==
==231996== HEAP SUMMARY:
==231996== in use at exit: 8 bytes in 1 blocks
==231996== total heap usage: 14 allocs, 13 frees, 1,800 bytes allocated
==231996==
==231996== 8 bytes in 1 blocks are definitely lost in loss record 1 of 1
==231996== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==231996== by 0x48E3BA7: operator new(unsigned long) (in /usr/lib/llvm-10/lib/libc++.so.1.0)
==231996== by 0x4028A8: tri::Area() const (in /home/ramcdona/Desktop/mwe)
==231996== by 0x401E57: mesh::Area() const (in /home/ramcdona/Desktop/mwe)
==231996== by 0x4017A9: main (in /home/ramcdona/Desktop/mwe)
==231996==
==231996== LEAK SUMMARY:
==231996== definitely lost: 8 bytes in 1 blocks
==231996== indirectly lost: 0 bytes in 0 blocks
==231996== possibly lost: 0 bytes in 0 blocks
==231996== still reachable: 0 bytes in 0 blocks
==231996== suppressed: 0 bytes in 0 blocks
==231996==
==231996== For lists of detected and suppressed errors, rerun with: -s
==231996== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
使用 gcc 构建,Valgrind 输出基本相同。
答:
避免这样做的一种方法是让它始终指向数据缓存,这可能是 .mutable
std::optional<double>
然后,您将创建并存储一个在对象的生命周期内保留的内容。std::unique_ptr<std::optional<double>>
tri
例:
#include <memory> // std::unique_ptr / std::make_unique
#include <optional> // std::optional
class tri {
public:
using cache_type = std::optional<double>;
tri() : m_Area(std::make_unique<cache_type>()) {} // create the cache
tri(const tri& rhs) : // copy constructor
m_Area(std::make_unique<cache_type>(*rhs.m_Area)),
m_P1(rhs.m_P1), m_P2(rhs.m_P2), m_P3(rhs.m_P3)
{}
tri(tri&&) noexcept = default; // move constructor
tri& operator=(const tri& rhs) { // copy assignment
m_Area = std::make_unique<cache_type>(*rhs.m_Area);
m_P1 = rhs.m_P1;
m_P2 = rhs.m_P2;
m_P3 = rhs.m_P3;
return *this;
}
tri& operator=(tri&& rhs) noexcept = default; // move assignment
// no user-defined destructor needed
void SetPts(const point& p1, const point& p2, const point& p3) {
m_P1 = p1;
m_P2 = p2;
m_P3 = p3;
m_Area->reset(); // the cache is not up to date anymore
}
double Area() const {
if(!*m_Area) *m_Area = CalcArea(); // set the cached value
return m_Area->value(); // return the stored value
}
private:
std::unique_ptr<cache_type> m_Area; // mutable not needed
point m_P1;
point m_P2;
point m_P3;
double CalcArea() const {
// the calculation
}
};
评论
std::optional
可能用于 。cache_t
cache_t
mutable
std::optional
正如 @Jarod42 所指出的,所写的赋值运算符是泄漏的来源。
缓存和可变设备都按预期工作。
更正后的代码应为:
tri & operator=( const tri & t )
{
if ( this != &t )
{
m_P1 = t.m_P1;
m_P2 = t.m_P2;
m_P3 = t.m_P3;
delete m_Area;
if ( t.m_Area )
{
m_Area = new double( *(t.m_Area) );
}
else
{
m_Area = NULL;
}
}
return *this;
}
@TedLyngmo建议的方法也将奏效。事实上,它可以完全避免这些问题。但是,我想知道为什么现有代码不起作用。
评论
mutable
mutable std::optional<double> m_Area;
评论
mutable std::unique_ptr<double> m_Area;
tri
不遵守 5/3/0 规则。泄漏与 无关。mutable
tri & operator=( const tri & t )
this->m_Area
std::optional<double>