【问题标题】:Why template function with 'const' from left of parameter type is misbehaving against the rule of type deduction for pointer argument?为什么参数类型左侧带有“const”的模板函数行为不符合指针参数的类型推导规则?
【发布时间】:2023-04-04 03:32:01
【问题描述】:

考虑这个用于类型推导的伪代码:

template<typename T> void f(ParamType param);

函数调用将是:f(expr);

根据类型推导情况,ParamType 不是引用、指针,也不是通用引用 (参见 S. Meyers “Effective Modern C++”,第 14 页),但是通过值传递,以确定类型 T,首先需要 忽略'expr'的引用和常量部分,然后模式匹配exprs类型来确定T。

司机将是:

void PerformTest() {

    int i = 42;
    int* pI = &i;
    f_const_left(pI);
    f_non_template_left(pI);
    f_const_right(pI);
    f_non_template_right(pI);
}

现在考虑这些函数,使用这个推导规则,在以指针作为参数调用时显示出一些违反直觉的结果:

template<typename T> void f_const_left(const T t) {
    // If 'expr' is 'int *' then, according to deduction rule for value parameter (Meyers p. 14),
    // we need to get rid of '&' and 'const' in exp (if they exist) to determine T, thus T will be 'int *'.
    // Hence, ParamType will be 'const int *'.
    // From this it follows that:
    //    1. This function is equivalent to function 'func(const int * t){}'
    //    2. If ParamType is 'const int *' then we have non-const pointer to a const object,
    //       which means that we can change what pointer points to but cant change the value
    //       of pointer address using operator '*'
    *t = 123;// compiler shows no error which is contradiction to ParamType being 'const int *'

    t = nullptr; // compiler shows error that we cant assign to a variable that is const

    // As we see, consequence 2. is not satisfied: 
    // T is straight opposite: instead of being 'const int *'
    // T is 'int const *'.
    // So, the question is:
    // Why T is not 'const int*' if template function is f(const T t) for expr 'int *' ?
}

考虑后果 1.:

让我们创建一个等效的非模板函数:

void f_non_template_left(const int* t) {
    // 1. Can we change the value through pointer?
    *t = 123; // ERROR: expression must be a modifiable lvalue
    // 2. Can we change what pointers points to?
    t = nullptr; // NO ERROR

    // As we can see, with non-template function situation is quite opposite.
}

为了实验的完整性,我们还考虑另一对函数,但从 T 的右侧放置“const”:一个模板函数及其非模板等效项:

template<typename T> void f_const_right(T const t) {
    // For expr being 'int *' T will be 'int *' and ParamType will be 'int * const',
    // which is definition of a constant pointer, which cant point to another address,
    // but can be used to change value through '*' operator.
    // Lets check it:

    // Cant point to another address:
    t = nullptr; // compiler shows error that we cant assign to a variable that is const

    // Can be used to change its value:
    *t = 123;
    // So, as we see, in case of 'T const t' we get 'int * const' which is constant pointer, which
    // is intuitive.
}

最后是类型右侧带有'const'的非模板函数:

void f_non_template_right(int* const t) {
    // 1. Can we change the value through pointer?
    *t = 123; // No errors
    // 2. Can we change what pointers points to?
    t = nullptr; // ERROR: you cant assign to a variable that is const

    // As we can see, this non-template function is equivalent to its template prototype
}

有人能解释一下为什么模板和非模板函数之间存在这种不一致吗? 为什么左侧带有'const'的模板函数的行为不符合演绎规则?

【问题讨论】:

  • "按类型扣除" 没有类型扣除; TParamType 似乎不相关。
  • const T 等价于std::add_const_t&lt;T&gt;。所以T = int*,它是int* const,而不是const int*(或int const*)。

标签: c++ c++11 templates c++14


【解决方案1】:

// 因此,ParamType 将是 'const int *'。

不,它不会(不仅仅是因为代码中没有使用ParamType。为了回答这个问题,我们假设您的意思是T,它就在那里)。

int * 是两个标记的序列。其中一个标记是类型名 (int),另一个是 *。当以这种方式组合时,这两个标记命名为一个类型:指向 int 的指针。

const int 是两个标记的序列。组合时,它们将类型命名为:const int

const int* 是 3 个标记的序列。当这样组合时,它们命名为单一类型。 C++ 类型命名的规则是,const 将“const-ness”应用于由其最左侧指定的类型。如果它的左边没有任何东西,它适用于紧邻右边的东西。因此,如果您将其视为表达式,则将其读取为(const int)*。因此,此标记序列将类型命名为:指向 const int 的指针。

const T,其中T 是类型名称(可以是模板参数、类型别名或类型名称),是两个标记的序列。组合时,它们命名一个类型:const(无论T 指定的类型)。

如果T 类型恰好命名为int*,则T 类型指定为“指向int 的指针”,如前所述。因此,const T 指定 'const (pointer to int)' 请注意 pointer 是 const,而不是 int 所指向的内容。

类型别名和模板参数的替换不是通过复制和粘贴标记来完成的。它是通过将类型名称规则作为一个单元应用于别名来完成的。无论T 是什么,像const 这样的限定词都适用于T 作为一个单元。

int * const 是三个标记的序列。根据上述规则,const 应用于其左侧的任何内容,如果有的话。所以const 适用于*。因此,这些标记将类型命名为:指向int 的常量指针。

T const 是两个标记的序列。当这样组合时,它们会命名一个类型:const(无论T 指定的类型)。

这就是为什么 T constint * const 行为相同,但 const Tconst int * 行为不同。

【讨论】:

    【解决方案2】:

    (参考 C++14 标准)

    您的 f_non_template_* 函数并不完全正确。

    由于T 是一个模板参数,它的行为就好像它是一个唯一类型:

    14.5.6.2 函数模板的部分排序

    (3) 为每个类型、非类型或模板模板参数(包括 模板参数包 (14.5.3))分别合成一个唯一的类型、值或类模板 并将其替换为模板函数类型中该参数的每次出现。

    所以要正确测试你的非模板函数需要这样定义:

    using TT = int*;
    
    void f_non_template_left(const TT t) {
        /* ... */
    }
    
    void f_non_template_right(TT const t) {
        /* ... */
    }
    

    godbolt example

    此时您将获得与模板化函数完全相同的行为。


    为什么会这样

    在这种情况下,T 将被推导出为 int*,作为唯一类型将是 复合类型

    3.9.2 复合类型

    (1) 复合类型可以通过以下方式构造:
    [...]
    (1.3) — 指向给定类型的 void 或对象或函数(包括类的静态成员)的指针
    [...]

    复合类型的 cv 规则如下:

    3.9.3 CV 限定符

    (1) 3.9.1 和 3.9.2 [Compound Type] 中提到的类型是 cv-unqualified 类型。每个类型都是 cv 不合格的完整 or 不完整的对象类型 or is void (3.9) 具有其类型的三个对应的 cv 限定版本:一个 const 限定版本、一个 volatile 限定版本和一个 const-volatile 限定版本。
    (2) 复合类型 (3.9.2) 不是由复合类型的 cv 限定符(如果有)进行 cv 限定的。应用于数组类型的任何 cv 限定符都会影响数组元素类型,而不是数组类型 (8.3.4)。

    因此,在您的模板函数中,T 的 cv 限定符在两种情况下都指复合类型 T 的顶级常量,所以

    template<typename T> void f_const_left(const T t);
    template<typename T> void f_const_right(T const t);
    

    实际上是等价的。

    唯一的例外是T 是数组类型,在这种情况下,cv 限定符将应用于数组的元素。


    如果你想指定指向值的常量,你可以这样做:

    //const value
    template<class T>
    void fn(const T* value);
    
    // const pointer
    template<class T>
    void fn(T* const value);
    
    // const value + const pointer
    template<class T>
    void fn(const T* const value);
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-04-12
      • 2014-04-17
      • 1970-01-01
      • 1970-01-01
      • 2016-12-16
      • 2016-12-30
      • 2021-08-15
      • 2012-08-09
      相关资源
      最近更新 更多