构造函数初始化列表之前的静态断言

11

有一个非模板类,其具有模板化构造函数。在这种构造函数中初始化成员变量之前是否可以检查静态断言?

例如,以下代码在检查T是否具有该方法之前执行了T::value()

class MyClass
{
public:
    template<typename T>
    MyClass(const T &t)
        : m_value(t.value())
    {
        static_assert(HasValueMethod<T>::value, "T must have a value() method");
    }

private:
    int m_value;
};

static_assert放在构造函数内部可以正常工作,但是它会在所有成员初始化列表中的错误消息之后打印出"T must have a value() method"。

prog.cpp: In instantiation of ‘MyClass::MyClass(const T&) [with T = int]’:
prog.cpp:24:16:   required from here
prog.cpp:12:21: error: request for member ‘value’ in ‘t’, which is of non-class type ‘const int’
         : m_value(t.value())
                   ~~^~~~~
prog.cpp:14:9: error: static assertion failed: T must have a value() method
         static_assert(HasValueMethod<T>::value, "T must have a value() method");
         ^~~~~~~~~~~~~

我觉得这有点令人困惑,想知道在初始化成员变量之前是否可以打印“T必须有一个value()方法”。 我知道我可以使用enable_if和SFINAE来禁用不适当的T构造函数,但我希望告诉用户比“找不到方法”更有意义的信息。

在构造函数体中的断言之后初始化成员变量? - tambre
@tambre,这个方法可能可行,但如果有可能我想避免使用它。 - Kane
1
@tambre:在函数体执行开始后,你无法初始化成员变量。 - Christian Hackl
2
要不考虑一下一个小的内联实用成员函数模板?在那里放置静态断言。然后写上 m_value(util_get_value(t))。相信 NRVO 会做正确的事情,完成了。 - StoryTeller - Unslander Monica
1
我想你可以使用enable_if来实现一个合适的构造函数,并且有一个备用构造函数,它只是断言你想要的消息。 - OriBS
3个回答

6
你可以使用std::enable_if来筛选掉具有函数成员value()的类型T,并将真正的实现保持分离基于static_assert的构造器。如果T具有value()方法,则选择第一个构造器,并按照通常的方式实现(除了需要std::enable_if才能被选中):
template <typename T, typename = std::enable_if_t<HasValueMethod<T>::value>>
MyClass(const T &t) : m_value(t.value())
{}

因此,我们需要第二个构造函数被SFINAEd排除在函数重载之外,因为第一个构造函数已经知道T::value的存在:

template <typename T, typename = std::enable_if_t<!HasValueMethod<T>::value>>
MyClass(const T &, ...)
{
  static_assert(HasValueMethod<T>::value, "T must have a value() method");
}

请注意可变参数...:它是必需的,以便区分构造函数的原型,使其不与第一个重叠(它们需要不同,否则会导致编译错误的模糊原型)。您不需要向其传递任何内容,只需将其设置为不同的原型即可。
同样要注意std::enable_if的谓词是相同的,但是被否定了。当HasValueMethod<T>::value为false时,第一个构造函数将被SFINAEd函数重载,但第二个构造函数不会,然后触发静态断言。
在静态断言的参数中仍然需要使用HasValueMethod<T>::value,因此它取决于执行T。否则,只需在那里放置false将始终触发,无论是否被选择出来。
以下是当T没有.value()时GCC打印的内容:
main.cpp: In instantiation of 'MyClass::MyClass(const T&, ...) [with T = A; <template-parameter-1-2> = void]':
main.cpp:35:18:   required from here
main.cpp:21:9: error: static assertion failed: T must have a value() method
         static_assert(HasValueMethod<T>::value, "T must have a value() method");

         ^~~~~~~~~~~~~

这是Clang的:

main.cpp:21:9: error: static_assert failed "T must have a value() method"
        static_assert(HasValueMethod<T>::value, "T must have a value() method");
        ^    

总之,这种方法存在一个问题(正如评论中@T.C.指出的那样):MyClass现在从未求值的上下文的角度来看可以转换为任何东西。也就是说,
static_assert(std::is_convertible_v</*anything*/, MyClass>); // Always true.

在C++20中,当有概念的希望时,这个问题可以很容易地通过使用requires子句来解决。
template <typename T>
  requires HasValueMethod<T>::value
MyClass(const T &t) : m_value(t.value())
{}

您可以直接在requires子句中表达HasValueMethod<T>,就像这样:
template <typename T>
  requires requires (T a) { { a.value() } -> int; }
MyClass(const T &t) : m_value(t.value())
{}

或将 HasValueMethod<T> 转化为一个真正的概念:

template <typename T>
concept HasValueMethod = requires (T a) {
    { a.value() } -> int;
};

// Inside `class MyClass`.
template <typename T>
  requires HasValueMethod<T>
MyClass(const T &t) : m_value(t.value())
{}

这些解决方案可以让std::is_convertible_v<T, MyClass>正常工作。

我认为,经过SFINAE处理的模板在重载决议期间不会被考虑。此外,在模板未实例化之前,模板中的static_assert没有任何效果。因此,在构造函数中不需要额外的参数,而static_assert(false)也应该可以工作。 - CAF
在重载决议期间,直到某些东西SFINAE它为止。 - Mário Feroldi
1
你的 MyClass 现在可以从任何东西中转换。 - T.C.
1
@T.C.:它已经是这样了,非法转换的自定义断言需要它。当测试可转换性时,这确实很不幸。 - Ben Voigt

4

static_assert() 更接近于使用。 在这种情况下,一个帮助函数可以实现:

class MyClass
{
    template<typename T>
    static int get_value(const T& t)
    {
        static_assert(HasValueMethod<T>::value, "T must have a value() method");
        return t.value();
    }

public:
    template<typename T>
    MyClass(const T &t)
        : m_value(get_value(t))
    {
    }

private:
    int m_value;
};

这不仅修正了错误消息的顺序,还可以让您在需要value()成员函数的每个路径上重复使用该消息。


现在我正在阅读问题评论(这些评论在审核队列中不可用),我看到StoryTeller已经建议了这种方法。 - Ben Voigt

4

如果您不打算对构造函数进行SFINAE约束,并且您希望在HasValueMethod为false时始终引发错误,那么您可以编写一个“硬”版本的特征类:

template<class T>
struct AssertValueMethod
{
  static_assert(HasValueMethod<T>::value, "T must have a value() method");
  using type = void; // note: needed to ensure instantiation, see below ...
};

template< typename T, typename = typename AssertValueMethod<T>::type >
MyClass(const T &t): ...

此外,如果以后您想添加一个 SFINEA 选择的重载,您可以始终编写一个适当的委托构造函数,而不必更改静态断言逻辑...

为什么要给我点踩?我有什么遗漏吗?请解释一下。 - Massimiliano Janes
我不是一个踩负分的人,这种方法似乎解决了问题,但在我看来有点过度设计。为什么需要从std::true_type继承?为什么需要一个模板别名? - Kane
@Kane,你必须实例化AssertValueMethodImpl才能触发static_assert;在这种情况下,通过其成员“type”获得隐式实例化,我选择了true_type中的一个来简洁明了。别名只是为了方便。我会编辑以澄清... - Massimiliano Janes
这个也好像可以工作:template<typename T> class AssertValueMethod { static_assert(...); };template<typename T, typename = AssertValueMethod<T>> MyClass(...) { ... }。或者我有什么地方错过了吗? - Kane
@Kane,这取决于'...',但那看起来像是编译器的错误。仅AssertValueMethod<whatever>的出现本身不应意味着它的隐式实例化,例如请参见此处 - Massimiliano Janes
@Kane,更具体地说,请参见temp.inst,其中第一段和后续段落了解更多详情。 - Massimiliano Janes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接