为什么 const 成员函数模板是针对非 const 对象实例化的?

Why is const member function template is instantiated for non-const object?

提问人:felps321 提问时间:8/28/2023 更新时间:8/29/2023 访问量:120

问:

我偶然发现了一个问题,即为非常量对象实例化了 const 成员函数模板,这会导致编译错误。 下面是可重现的示例:

#include <cstdio>
#include <type_traits>

struct S {
    using Data = int;

    Data data{};

    template <typename F> requires std::is_invocable_v<F, Data&>
    void func(F)
    {
        puts("S::func(F)");
    }

    template <typename F> requires std::is_invocable_v<F, const Data&>
    void func(F) const
    {
        puts("S::func(F) const");
    }
};

struct FuncObj {
    template <typename T>
    auto operator()(T&)
    {
        // imaginary code that uses T assuming that it's non-const
        static_assert(!std::is_const_v<T>);
    }
};

int main()
{
    S s;
    s.func(FuncObj());
}

编译此代码时,出现“静态断言失败”错误。 现在,我确实知道这个错误是由存在和需要的返回类型引起的 要推导,因此需要实例化模板函数体,我从这个问题中了解到这一点:为什么 std::is_invocable 不与自动推导的返回类型(例如通用 lambda)一起使用模板化 operator()。FuncObj::operator()auto

我确实理解这部分。但是我不明白的是,为什么编译器在不是 const 对象时甚至考虑 const 成员函数 ()? 如果编译器只是推导/实例化(非常量成员函数),则不会有错误。 此外,如果我在函数中使用相同的条件,而不是(或 SFINAE), 错误消失了:S::func(F) constS sS::func(F)requiresvoid func(F) conststatic_assert

    template <typename F> 
    void func(F) const // requires std::is_invocable_v<F, const Data&>
    {
        static_assert(std::is_invocable_v<F, const Data&>);
        puts("S::func(F) const");
    }

这似乎非常不直观,我想不出任何理由来解释为什么编译器会在有非常量函数时尝试成员函数的版本? C++ 标准中是否有任何地方对其进行了解释,以便我能够理解这一点?const

C++ 模板 17 C++ 20

评论

1赞 StoryTeller - Unslander Monica 8/28/2023
std::is_invocable_v需要做过载解决。否则它将如何给出答案?由于必须实例化这种手段...剩下的你就知道了。operator()operator()
0赞 StoryTeller - Unslander Monica 8/28/2023
下面是一个完全最小的示例来证明这一点:godbolt.org/z/xYba4hqs5 - 返回类型扣除根本不友好。auto
0赞 felps321 8/28/2023
@StoryTeller-UnslanderMonica 我在问题中写道,我知道这个甚至链接的问题,我在问为什么编译器在非常量对象上考虑 const-member 函数?
0赞 felps321 8/28/2023
我的问题与 SFINAE 无关,而是关于为什么编译器甚至考虑非 const 对象的成员函数的 const 重载,当存在非 const 成员函数时
1赞 StoryTeller - Unslander Monica 8/28/2023
因为您可以在非常量对象上调用 const 函数。这使它成为过载解决的可行候选者,因此将在过载解决中对其进行检查。有决胜局的事实不会跳过检查,因为首先考虑决胜局的是检查。

答:

2赞 Jan Schultke 8/29/2023 #1

首先,您已经正确地理解了 的两个重载都是成员函数调用的候选者。funcs.func(FuncObj());

编译器必须检查第二个重载是否为 true,这涉及检查格式是否正确,其中 is 的类型为 。 存在并不妨碍 A 绑定到它。它只是意味着推导为 .目前还没有任何东西被淘汰出重载集。std::is_invocable_v<F, const Data&>F(D)Dconst Data&Dconst Data&T&Tconst Data

因为是 ,并且有推导的返回类型,所以需要实例化它。 在实例化期间:FFuncObjFuncObj::operator()

template <typename T> // T  = const Data
auto operator()(T&)   // T& = const Data&
{
    // std::is_const_v<const Data> is true, so this static assertion fails.
    // The program is ill-formed because a static_assertion has failed.
    static_assert(!std::is_const_v<T>);
}

编译器实际上会准确地告诉你这个错误发生在哪里:

<source>:27:23: error: static assertion failed due to requirement '!std::is_const_v<const int>'
   27 |         static_assert(!std::is_const_v<T>);
      |                       ^~~~~~~~~~~~~~~~~~~

  [...]

<source>:15:36: note: while substituting template arguments into constraint expression here
   15 |     template <typename F> requires std::is_invocable_v<F, const Data&>
      |                                    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:34:7: note: while checking constraint satisfaction for template 'func<FuncObj>' required here
   34 |     s.func(FuncObj());
      |       ^~~~

通常,对 SFINAE 不友好,函数体中的任何断言失败都会使程序格式不正确,而不是将候选者从重载集中剔除。 它被称为“替换失败不是错误”,而不是“实例化失败不是错误”。static_assert

正如你所指出的,你可以这样写:

template <typename T> requires (!std::is_const_v<T>)

这将编译时没有错误。如果您使用而不是推导的返回类型,那也很好,因为检查表达式有效性不会涉及实例化函数模板。void

评论

0赞 felps321 8/29/2023
谢谢,现在我明白了。问题是我的代码使用用户提供的回调,通常是 lambda,因此默认情况下它具有返回类型,并且我不想在我的代码中记录所有这些行为并强制用户使用约束或,所以我将坚持从 const 成员函数中删除并使其成为内部 const 成员函数体。auto-> voidrequiresstatic_assert
0赞 Jan Schultke 8/29/2023
@felps321 checking whether something is is a code smell anyway. What you really want is to check whether a type is assignable, for instance. Even if it's not , it could still be effectively immutable, so very little is gained by checking it. I don't think it's unreasonable to document that the user's callbacks need to be properly constrained. If the user provides a lambda , (not using ), then it can't be called with a . Using non-generic lambdas can help here.constconst[](Data&) {}autoconst Data&
0赞 felps321 8/29/2023
yeah it's a code smell but the problem is that in my real code there is no const checks or static assertions, user callback just instantiated some template with from generic lambda and this instantiation failed with multi-thousand line error message.T
0赞 felps321 8/29/2023
all because user code assumed that T is non-const and rightfully so because function that takes a callback was called on a non-const object. For example imagine you implement some value container type and you provide a function to access this value through a callback. Now imagine that callback wants to move the value, it will fail for because when is d value will be const. All because this value container class has const overload.std::unique_ptrTconst std::unique_ptr&std::move