c++g++c++17clangstd-filesystem

std::unordered_set<std::filesystem::path>: compile error on clang and g++ below v.12. Bug or user error?


I added the following function template to my project, and a user complained that it wouldn't compile on their system anymore:

template<typename T>
std::size_t removeDuplicates(std::vector<T>& vec)
{
    std::unordered_set<T> seen;

    auto newEnd = std::remove_if(
        vec.begin(), vec.end(), [&seen](const T& value)
        {
            if (seen.find(value) != std::end(seen))
                return true;

            seen.insert(value);
            return false;
        }
    );

    vec.erase(newEnd, vec.end());

    return vec.size();
}

The error message with g++ 9.4 was approximately

error: use of deleted function
'std::unordered_set<_Value, _Hash, _Pred, _Alloc>::unordered_set() 
[with
_Value = std::filesystem::__cxx11::path; 
_Hash = std::hash<std::filesystem::__cxx11::path>; 
_Pred = std::equal_to<std::filesystem::__cxx11::path>; 
_Alloc = std::allocator<std::filesystem::__cxx11::path>]'
   12 |  std::unordered_set<T> seen;

So the error arose when instantiating the above function template with T = std::filesystem::path. I investigated a little and found that there was no issue when instantiating it with other types, e.g. fundamental types or std::string, but only with std::filesystem::path.

Using Compiler Explorer, I looked at how different compiler versions treat the code and found that only g++ v.12 can compile the instantiation with std::filesystem::path. Any g++ version below 12 fails with the above error. clang produces a similar error (call to implicitly deleted default constructor), even on the newest version (14). I didn't test other compilers.

The workaround I used was substituting std::unordered_set with std::set. Then it works on g++ v.8 and clang v.7 and above.

So I guess the error is a missing hashing function for std::filesystem::path? Or is it an error on my part?


Solution

  • The std::hash specialization for std::filesystem::path has only recently been added as resolution of LWG issue 3657 into the standard draft. It hasn't been there in the published C++17 and C++20 standards.

    There has always been however a function std::filesystem::hash_value from which you can easily create a function object to pass as hash function to std::unordered_set:

    struct PathHash {
        auto operator()(const std::filesystem::path& p) const noexcept {
            return std::filesystem::hash_value(p);
        }
    };
    
    //...
    
    std::unordered_set<std::filesystem::path, PathHash> seen;
    

    If you are providing that template without any guarantees that it works on types other than those that have a std::hash specialization defined, then I think there is no problem on your part.

    However, if you require the type to be hashable, it would be a good idea to let the user override the hash function used the same way std::unordered_set does. The same also applies to the equality functor used.