Home > OS >  Defaulted concept-constrained function never selected for instantiation
Defaulted concept-constrained function never selected for instantiation

Time:04-07

While working with C 20, I encountered an oddity which I'm unsure is a compiler defect due to C 20's infancy, or whether this is correct behavior.

The problem is that a specific constrained function is either not selected correctly, or produces a compile-error -- depending entirely on the order of the definitions.

This occurs in specific circumstance:

  • A constructor / destructor / member-function is constrained by requires, and
  • This constructor / destructor / member-function is implicitly deleted for some instantiation of T that does not satisfy the requires clause

For example, consider this basic naive_optional<T> implementation:

template <typename T>
class naive_optional {
public:
    naive_optional() : m_empty{}, m_has_value{false}{}

    ~naive_optional() requires(std::is_trivially_destructible_v<T>) = default;
    ~naive_optional() {
        if (m_has_value) {
            m_value.~T();
        }
    }
private:
    struct empty {};
    union {
        T m_value;
        empty m_empty;
    };
    bool m_has_value;
};

This code works fine for trivial types like naive_optional<int>, but fails to instantiate for non-trivial types such as naive_optional<std::string> due to an implicitly deleted destructor:

<source>:26:14: error: attempt to use a deleted function
    auto v = naive_optional<std::string>{};
             ^
<source>:8:5: note: explicitly defaulted function was implicitly deleted here
    ~naive_optional() requires(std::is_trivially_destructible_v<T>) = default;
    ^
<source>:17:11: note: destructor of 'naive_optional<std::basic_string<char>>' is implicitly deleted because variant field 'm_value' has a non-trivial destructor
        T m_value;
          ^
1 error generated.

Live Example

If the order of definitions is changed so that the constrained function is below the unconstrained one, this works fine -- but instead selects the unconstrained function every time:

...
    ~naive_optional() { ... }
    ~naive_optional() requires(std::is_trivially_destructible_v<T>) = default;
...

Live Example


My question is ultimately: is this behavior correct -- am I misusing requires? If this is correct behavior, is there any simple way to accomplish conditionally default-ing a constructor / destructor / function that is not, itself, a template? I was looking to avoid the "traditional" approach of inheriting base-class implementations.

CodePudding user response:

Your code is correct, and indeed was the motivating example for supporting this sort of thing (see P0848, of which I am one of the authors). Clang simply doesn't implement this feature yet (as you can see here).

gcc and msvc both accept your code.


Note that you don't need empty anymore, just union { T val; } is sufficient in C 20, thanks to P1331. Demo.

  • Related