根据 class 的特征禁用 class 构造函数的正确方法

A proper way to disable a class constructor based on a trait of that class

我想 SFINAE 禁用 class 模板构造函数 - 但要注意的是,启用 - 禁用条件是 class 的特征。特别是,我只想在 class.

中存在与构造函数的模板参数匹配的特定 class 方法时才启用该构造函数

一个简化的例子:

struct S
{
    std::size_t n = 0;
    void set_value(int i) { n = i; }
    void set_value(const std::string & s) { n = s.size(); }
    template <class T, class = decltype(std::declval<S>().set_value(std::declval<T>()))>
    S(T && x)
    {
        set_value(std::forward<T>(x));
    }
};

该解决方案适用于 GCC,据称适用于 MSVC,但不适用于 Clang。在我看来 Clang 就在这里,因为在 class 定义中 class 尚未定义,所以 std::declval<S>() 不应该在这里工作。

所以我的想法是将该构造函数定义移出 class 定义:

struct S
{
    std::size_t n = 0;
    void set_value(int i) { n = i; }
    void set_value(const std::string & s) { n = s.size(); }
    template <class T, class>
    S(T &&);
};

template <class T, class = decltype(std::declval<S>().set_value(std::declval<T>()))>
S::S(T && x)
{
    set_value(std::forward<T>(x));
}

但这在 GCC 和 MSVC 中不起作用。该解决方案也看起来有些可疑,因为我首先无条件声明该构造函数,然后才引入 SFINAE。

我的问题是 - 是否有可能在 C++11/14/17 中解决该任务?一个特定的任务是 class 只有当它也有一个匹配的 setter 方法时才应该有一个构造函数。显然,可以为每个 setter 方法重载手动定义一个非模板构造函数,但这很乏味且难以维护。

但我也对更一般的任务感兴趣 - SFINAE - 通过 class 的特征启用构造函数或其他 class 方法(需要完整的 class 定义) .

此外,在我的示例中哪个编译器更合适 - GCC/MSVC 还是 Clang?

您可以引入另一个默认值为S的模板参数,并将其用于SFINAE。例如

template <class T, class X = S, class = decltype(std::declval<X>().set_value(std::declval<T>()))>
S(T && x)
{
    set_value(std::forward<T>(x));
}

LIVE

关于哪个编译器是正确的问题,这是CWG1836

问题不在于 std::declval:您可以将 std::declval<S> 与不完整的 S 一起使用,因为它 returns 一个 S&&。问题是当您使用 std::declval<S>().set_value 时,因为 S 在那一刻不完整。

但是,有一个明确的例外:

the class type shall be complete unless the class member access appears in the definition of that class.

并且 class 成员访问 是否出现在 S 的定义中。

所以 gcc 和 msvc 允许它是正确的。


好像clang不适用CWG1836的决议。在那种情况下,它发现 std::declval<S>() 没有依赖类型,因此它可以立即评估成员访问 std::declval<S>().set_value(但没有这样做)。

为了安抚clang,可以将其设为依赖类型。例如,由于您已经在模板中,您可以使它依赖于 T 是什么:

template<typename T, typename...>
struct dependent_type_identity {
    using type = T;
};

template<typename T, typename... Dependence>
using dependent_type_identity_t = typename dependent_type_identity<T, Dependence...>::type;

struct S
{
    std::size_t n = 0;
    void set_value(int i) { n = i; }
    void set_value(const std::string & s) { n = s.size(); }
    template <class T, class = decltype(std::declval<dependent_type_identity_t<S, T>>().set_value(std::declval<T>()))>
    S(T && x)
    {
        set_value(std::forward<T>(x));
    }
};

或者做 做的事情,并使它依赖于另一个默认为 S 的模板参数(它仍然是一个依赖类型,即使没有办法改变它是什么设置为)