Why does overload resolution pick the wrong overload when providing a template argument explicitly?

131 Views Asked by At

My code is here:

#include <iostream>
#include <memory>
#include <queue>

template<typename TValue>
[[maybe_unused]]
constexpr auto t1(const std::queue<TValue> &value) -> void {
    std::queue<TValue> temp = value;
    while (!temp.empty()) {
        std::cout << temp.front() << std::endl;
        temp.pop();
    }
}

template<typename TValue>
constexpr auto t1(const TValue &nm) -> void {
    std::cout << nm << std::endl;
}

template<typename TValue>
constexpr auto t1(const std::shared_ptr<TValue> &nm) -> void {
    t1<TValue>(*nm);
}


int main(int argc, char **argv) {
    std::shared_ptr<const int> s_ptr = std::make_shared<const int>(7);
    t1(s_ptr);

    return 0;
}

This code fails to compile (https://godbolt.org/z/crvKb7rEz):

error C2338: static_assert failed: 'The C++ Standard forbids containers of const elements because allocator is ill-formed.'

I've tried to change template with shared_ptr like:

template<typename TValue>
constexpr auto t1(const std::shared_ptr<TValue> &nm) -> void {
    const int temp = *nm;
    t1<TValue>(temp);
}

It results in the same error. I've also tried to get the 'TValue' type:

template<typename TValue>
constexpr auto t1(const std::shared_ptr<TValue> &nm) -> void {
    if constexpr (std::is_same_v<TValue, const int>){
        static_assert(false, "??");
    }
    t1<TValue>(*nm);
}

static_assert is triggered. That means 'TValue' is 'const std::string'.

Accidentally, I've removed <TValue> like this:

template<typename TValue>
constexpr auto t1(const std::shared_ptr<TValue> &nm) -> void {
    t1(*nm);
}

Or:

template<typename TValue>
constexpr auto t1(const std::shared_ptr<TValue> &nm) -> void {
    t1<std::remove_cv_t<TValue>>(*nm);
}

Also:

template<typename TValue>
constexpr auto t1(const TValue &nm) -> void {
    std::cout << nm << std::endl;
}

template<typename TValue>
constexpr auto t1(const std::shared_ptr<TValue> &nm) -> void {
    t1<TValue>(*nm);
}


int main(int argc, char **argv) {
    std::shared_ptr<const int> s_ptr = std::make_shared<const int>(7);
    t1(s_ptr);

    return 0;
}

All of these work.

Why does the template wants to use std::queue overload when <TValue> euqal to const int? I except that it uses:

template<typename TValue>
constexpr auto t1(const TValue &nm) -> void {
    std::cout << nm << std::endl;
}
1

There are 1 best solutions below

3
Jan Schultke On BEST ANSWER

The problem is that you're explicitly providing the template argument TValue which results in an instantiation of std::deque<const std::string>. std::deque<T> requires a non-const, non-volatile T, as the failed static assertion says:

error: static assertion failed: std::deque must have a non-const, non-volatile value_type

- https://godbolt.org/z/PGY7nKW57

Minimal Reproducible Example

To illustrate what's going on, here's a simplified version of your problem (https://godbolt.org/z/zYPhhb5dq):

#include <type_traits>

template <typename T>
struct container {
    static_assert(not std::is_const_v<T>);
};

template<typename T> void foo(container<T>);
template<typename T> void foo(T);

int main() {
    foo<const int>(0);
}
error: static assertion failed due to requirement '!std::is_const_v<const int>'
    5 |     static_assert(not std::is_const_v<T>);
      |                   ^~~~~~~~~~~~~~~~~~~~~~
note: in instantiation of template class 'container<const int>' requested here
   15 |     foo<const int>(0);
      |

Solution

You can write t1(*nm) instead. The difference here is that std::deque<const std::string> is never instantiated. Instead, substitution into t1(std::deque) would fail because *nm is a const std::string and TValue in std::deque<TValue> cannot be deduced from const std::string.

template<typename TValue>
constexpr auto t1(const std::shared_ptr<TValue> &nm) -> void {
    t1(*nm); // OK
}

t1<std::remove_cv_t<TValue>>(*nm); also works (though you shouldn't write it) because you instantiate std::deque<std::string> instead. This is valid, however, overload resolution will pick t1(const T&) because there is no implicit conversion from std::string to std::deque.

You should write t1(*nm); It's generally better to deduce template parameters, not provide them explicitly.


Note on SFINAE-friendliness

The underlying issue is that static_assert isn't SFINAE-friendly, i.e. it results in errors that come after substitution. In C++20, it is possible to use constraints instead of static_assert while having the same template parameters:

template <typename T>
  requires (not std::is_const_v<T>) // analogous for std::deque
struct container { };

If this is done, foo<const int>(0) is valid (https://godbolt.org/z/TWKxs99Yj).

I don't believe it is legal for std::deque to have such constraints. If it had them, your code would work.