c++stdmapmutableconst-iterator

Logical const in a container in C++


Edited to include MWE (removing example-lite) and added details about compilation and Valgrind output.

I am using the mutable keyword to achieve the result of lazy evaluation and caching a result. This works fine for a single object, but doesn't seem to work as expected for a collection.

My case is more complex, but let's say I have a triangle class that can calculate the area of a triangle and cache the result. I use pointers in my case because the thing being lazily evaluated is a more complex class (it is actually another instance of the same class, but I'm trying to simplify this example).

I have another class that is essentially a collection of triangles. It has a way to calculate the total area of all the contained triangles.

Logically, tri::Area() is const -- and mesh::Area() is const. When implemented as above, Valgrind shows a memory leak (m_Area).

I believe since I am using a const_iterator, the call to tri::Area() is acting on a copy of the triangle. Area() is called on that copy, which does the new, calculates the area, and returns the result. At that point, the copy is lost and the memory is leaked.

In addition, I believe this means the area is not actually cached. The next time I call Area(), it leaks more memory and does the calculation again. Obviously, this is non-ideal.

One solution would be to make mesh::Area() non-const. This isn't great because it needs to be called from other const methods.

I think this might work (mark m_Triangles as mutable and use a regular iterator):

However, I don't love marking m_Triangles as mutable -- I'd prefer to keep the compiler's ability to protect the constiness of m_Triangles in other un-related methods. So, I'm tempted to use const_cast to localize the ugly to just the method that needs it. Something like this (mistakes likely):

Not sure how to implement with const_cast -- should I be casting m_Triangles or this? If I cast this, is m_Triangles visible (since it is private)?

Is there some other way that I'm missing?

The effect I want is to keep mesh::Area() marked const, but have calling it cause all the tris calculate and cache their m_Area. While we're at it -- no memory leaks and Valgrind is happy.

I've found plenty of examples of using mutable in an object -- but nothing about using that object in a collection from another object. Links to a blog post or tutorial article on this would be great.

Thanks for any help.

Update

From this MWE, it looks like I was wrong about the point of the leak.

The code below is Valgrind-clean if the call to SplitIndx() is removed.

In addition, I added a simple test to confirm that the cached value is getting stored and updated in the container-stored objects.

It now appears that the call m_Triangles[indx] = t1; is where the leak occurs. How should I plug this leak?

#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;
}

Compiled with:

clang++ -Wall -std=c++11 -stdlib=libc++ mwe.cpp -o mwe

Or:

g++ -Wall -std=c++11 mwe.cpp -o mwe

Valgrind output (from 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)

Built with gcc, the Valgrind output was essentially the same.


Solution

  • As pointed out by @Jarod42, the assignment operator as written is the source of the leak.

    The cache and mutable are all working as expected.

    The corrected code should read:

        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;
        }
    

    The approach suggested by @TedLyngmo would also work. In fact, it would avoid these sorts of problems entirely. However, I was looking to understand why the existing code did not work.