C++ 容器中的逻辑常量

Logical const in a container in C++

编辑以包含 MWE(删除 example-lite)并添加了有关编译和 Valgrind 输出的详细信息。

我正在使用 mutable 关键字来实现延迟计算的结果并缓存结果。这对于单个对象来说工作正常,但对于集合似乎并不像预期的那样工作。

我的情况比较复杂,但假设我有一个三角形 class 可以计算三角形的面积并缓存结果。我在我的案例中使用了指针,因为被惰性评估的东西是一个更复杂的 class(它实际上是相同 class 的另一个实例,但我试图简化这个例子)。

我还有一个 class,它本质上是一组三角形。它有一种方法可以计算所有包含的三角形的总面积。

逻辑上,tri::Area() 是常量——而 mesh::Area() 是常量。当如上实现时,Valgrind 显示内存泄漏 (m_Area)。

我相信因为我使用的是 const_iterator,所以对 tri::Area() 的调用作用于三角形的副本。 Area() 在该副本上调用,它执行新操作,计算面积,然后 returns 结果。到那时,副本丢失,内存泄漏。

此外,我认为这意味着该区域实际上并未缓存。下次我调用 Area() 时,它会泄漏更多内存并再次进行计算。显然,这是不理想的。

一个解决方案是使 mesh::Area() 成为非常量。这不是很好,因为它需要从其他 const 方法调用。

我认为这可能有效(将 m_Triangles 标记为可变并使用常规迭代器):

但是,我不喜欢将 m_Triangles 标记为可变的——我更愿意保留编译器在其他不相关的方法中保护 m_Triangles 的连续性的能力。所以,我很想使用 const_cast 将丑陋的地方定位到只需要它的方法。像这样(可能会出错):

不确定如何使用 const_cast 来实现——我应该使用 m_Triangles 还是这个?如果我施放这个,m_Triangles 是否可见(因为它是私有的)?

我还缺少其他方法吗?

我想要的效果是保持 mesh::Area() 标记为 const,但调用它会导致所有 tris 计算并缓存它们的 m_Area。当我们这样做时——没有内存泄漏,Valgrind 很高兴。

我发现了很多在对象中使用可变对象的例子——但没有找到在另一个对象的集合中使用该对象的例子。链接到博客 post 或关于此的教程文章会很棒。

感谢您的帮助。

更新

从这个 MWE 看来,我对泄漏点的看法是错误的。

如果删除对 SplitIndx() 的调用,下面的代码是 Valgrind-clean。

此外,我添加了一个简单的测试来确认缓存值正在容器存储对象中存储和更新。

现在看来调用 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
    }
};

正如@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 建议的方法也可以。事实上,它可以完全避免这些问题。但是,我想了解为什么现有代码不起作用。