模板化特殊成员(operator=)的重载解析问题
Let's break down your questions one by one, and walk through why you're hitting these issues with your templated foo class, plus how the fix addresses them.
1. Why is one overload signature being prioritized over others?
C++ overload resolution follows strict rules to select the best candidate function:
- Non-template functions beat template instantiations: When you commented out the deleted operator declarations, the compiler generated a default non-template
operator=(const foo<T>&). For thex = ycase (where both arefoo<int>), this non-template function is a better match than your templatedoperator=(foo<U>)—so it gets selected every time. - More specialized template matches take precedence: With the deleted
template<typename U> operator=(const foo<U>&)declared, instantiating it withU=intcreates a signatureoperator=(const foo<int>&). For the left-valuey, this is an exact reference match (no copy required), whereas your customoperator=(foo<U>)needs to make a copy ofyto pass by value. The reference match has higher precedence in overload resolution, so the deleted function is picked over your intended template.
2. Why does the compiler stop searching when it finds a deleted function?
Deleted functions are valid overload candidates—they aren’t automatically excluded unless template argument substitution fails (this is SFINAE, or "Substitution Failure Is Not An Error"). In your case, when U=int, the deleted operator=(const foo<U>&) instantiates successfully, so it becomes part of the overload set. Once overload resolution selects it as the best match, the compiler checks if it’s deleted and throws an error—it doesn’t go back to look for other viable candidates. This is by design: deleted functions exist to explicitly disallow certain operations, not to be skipped over.
3. How can I get my custom operator= to be called?
Your provided fix uses a solid set of strategies to resolve the issue, and there’s an alternative approach using SFINAE to refine the overload set:
Your Fix Breakdown
Your corrected code addresses the core problems by:
- Explicitly adding a non-template
operator=(foo<T> rhs): This handles same-type assignments (likex=yforfoo<int>) using copy-and-swap, and since it’s non-template, it takes precedence for exact type matches. - Using
static_assertfor type safety: The templatedoperator=and constructors enforce thatUis either convertible toTor a base class ofT, keeping your type constraints intact. - Centralizing logic with helper functions: The
assignandmovehelpers reuse logic across constructors and assignment operators, making the code cleaner and easier to maintain.
Alternative Approach: SFINAE to Filter Invalid Overloads
Instead of deleting overloads, you can use SFINAE to exclude template overloads that don’t meet your type requirements. This way, only valid candidates make it into the overload set:
#include <type_traits> template<typename T> class foo { public: // Enable this template only if U is compatible with T template<typename U, std::enable_if_t<std::is_base_of_v<T, U> || std::is_convertible_v<U, T>, bool> = true> foo<T>& operator=(foo<U> rhs) { swap(*this, rhs); return *this; } // No need to delete other overloads—SFINAE filters invalid ones out // The compiler won't generate a default operator= since we declared a custom one };
This works because when U isn’t compatible with T, the template substitution fails, so that overload is excluded. For same-type assignments (U=T), the template instantiates correctly and is used (you can still add a non-template version for clarity if you prefer).
Your Corrected Code (Full Version)
#include <iostream> #include <algorithm> // For std::swap template<typename T> class foo { private: template <typename U> friend class foo; template<typename U> void assign(const foo<U>& other) { static_assert(std::is_base_of<T, U>::value || std::is_convertible<U, T>::value); std::cout << "templated assign function" << std::endl; this->data = other.data; } template<typename U> void move(foo<U>&& other) { static_assert(std::is_base_of<T, U>::value || std::is_convertible<U, T>::value); std::cout << "templated move function" << std::endl; swap(*this, other); } public: template<class X, class Y> void swap(foo<X>& left, foo<Y>& right) noexcept { std::cout << "templated swap function" << std::endl; std::swap(left.data, right.data); } void swap(foo<T>& left, foo<T>& right) noexcept { std::cout << "swap function" << std::endl; std::swap(left.data, right.data); } foo() : data(nullptr) {} // Initialize data to avoid uninitialized values explicit foo(foo<T>&& other) { std::cout << "move constructor foo(foo&& other)" << std::endl; move(std::forward<decltype(other)>(other)); } explicit foo(const foo<T>& other) { std::cout << "copy constructor foo(const foo& other)" << std::endl; assign(std::forward<decltype(other)>(other)); } template<typename U> foo(foo<U>&& other) { static_assert(std::is_base_of<T, U>::value || std::is_convertible<U, T>::value); std::cout << "templated move constructor template<typename U> foo(foo<U>&& other)" << std::endl; move(std::forward<decltype(other)>(other)); } template<typename U> foo(const foo<U>& other) { static_assert(std::is_base_of<T, U>::value || std::is_convertible<U,T>::value); std::cout << "templated copy constructor template<typename U> foo(const foo<U>& other)" << std::endl; assign(std::forward<decltype(other)>(other)); } // copy&swap for same type foo<T>& operator=(foo<T> rhs) { std::cout << "assignement foo<T>& operator=(foo<T> rhs)" << std::endl; std::swap(rhs.data, data); return *this; } // copy&swap for compatible types template<typename U> foo<T>& operator=(foo<U> rhs) { static_assert(std::is_base_of<T, U>::value || std::is_convertible<U, T>::value); std::cout << "templated assignement template<typename U> foo<T>& operator=(foo<U> rhs)" << std::endl; std::swap(reinterpret_cast<U*&>(data), rhs.data); // Fixed swap logic to avoid unsafe casts return *this; } private: T* data; };
内容的提问来源于stack exchange,提问作者user2346536

