c++valgrindsdl-2std-rangessdl-ttf

Valgrind thinks my std::ranges of raw pointers are leaking, even after they've been copied to unique pointers


I'm trying to load all true-type fonts in a directory using C++20 ranges and functional-style programming. However, since fonts are a resource, I'm allocating memory within the ranges interface. I think this is why valgrind thinks I have a leak. I have a few std::views of freshly allocated raw pointers that eventually get discarded - however, these raw pointers are transformed and copied into a vector of unique pointers.

The code in question:

// free a font resource
struct font_deleter {
    void operator()(TTF_Font * font) { TTF_CloseFont(font); }
};

// aliases
using unique_font = std::unique_ptr<TTF_Font, font_deleter>;
using font_table = std::unordered_map<std::string, TTF_Font *>;

template<typename expected_t>
using result = tl::expected<expected_t, std::string>;

// determine if a path is a valid font file
auto _is_font_fxn(std::error_code & ec) {
    return [&ec](fs::path const & path) {
        return fs::is_regular_file(path, ec) and path.extension() == ".ttf";
    };
}

// load a font-name, font-pointer pair from a font file
font_table::value_type _load_as_pair(fs::path const & path)
{
    return std::make_pair(path.stem(), TTF_OpenFont(path.c_str(), 100));
}

// create a unique font pointer from a name-pointer pair
unique_font _ufont_from_pair(font_table::value_type const & pair)
{
    return unique_font(pair.second, font_deleter{});
}

// determine if a font pointer is null from a name-pointer pair
bool _font_is_null(font_table::value_type const & pair)
{
    return pair.second == nullptr;
}

result<std::vector<unique_font>>
button_factory::load_all_fonts(fs::path const & dir)
{
    namespace views = std::views;
    namespace ranges = std::ranges;

    std::error_code ec; // using error codes tells filesystem not to throw

    // make sure the path exists and is a valid directory
    if (not fs::exists(dir, ec) or not fs::is_directory(dir, ec)) {
        std::stringstream message;
        if (ec) { message << ec.message(); } // an os call failed
        else if (not fs::exists(dir)) { message << dir << " doesn't exist"; }
        else if (not fs::is_directory(dir)) { message << dir << " isn't a directory"; }
        return tl::unexpected(message.str());
    }

    // recursively get all paths in directory
    std::vector<fs::path> paths(fs::recursive_directory_iterator(dir, ec), {});

    // filter only font files and load them as name-font pairs
    auto is_font = _is_font_fxn(ec);
    auto fonts = paths | views::filter(is_font) | views::transform(&_load_as_pair);

    // put all the successfully loaded fonts into a vector of unique pointers
    // the font resources are freed automatically if returning unexpected
    // otherwise this is the expected result
    std::vector<unique_font> font_handle;
    auto into_handles = std::back_inserter(font_handle);
    ranges::transform(fonts, into_handles, &_ufont_from_pair);

    // abort if any os calls failed in the process, or if some fonts didn't load
    if (ec) { return tl::unexpected(ec.message()); }
    if (ranges::any_of(fonts, &_font_is_null)) { return tl::unexpected(TTF_GetError()); }

    // add all the loaded fonts definitions to the button factory
    auto into_table = std::inserter(_fonts, _fonts.end());
    ranges::copy(fonts, into_table);

    return font_handle;
}

As a side note, I'm using TartanLLama's implementation of the proposed std::expected, which is where the tl:: namespace comes from.

I'm getting some gnarly template error messages from valgrind, so I'll try to just share the important bits. 62,000 bytes are lost in two blocks, and while the template error is a nightmare, it seems to all be coming from the ranges interface. It seems like the message in the first block is coming from the call to ranges::any_of and the second one from ranges::copy - both blocks mention _load_pair - I'm guessing as the source of the memory that's leaking.

Is there some reason I'm not seeing about why this might leak memory, or is this a valgrind bug?


Solution

  • views::transform is lazy - the transform function isn't called until an element of the view is accessed. But that also means that the transform function is called every time an element of the view is accessed.

    So every time you iterate through fonts - first for the transform, then for the any_of, and finally for the copy, you are calling _load_as_pair and therefore TTF_OpenFont again - and leaking the result.