65.9K
CodeProject 正在变化。 阅读更多。
Home

C++11 中的常量与常量表达式

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2012年7月8日

CPOL

8分钟阅读

viewsIcon

76033

downloadIcon

210

关键字:constexpr, constant, constant expression, user-defined literals, GCC 4.7.0

引言

本文讨论了有关常量定义 (const) 和常量表达式 (constexpr) 的问题。有关常量的示例在 GCC 4.7.0、Visual Studio 2008、Visual Studio 2010 和 Visual Studio 11 中运行。涉及 constexpr 的示例在 GCC 4.7.0 中运行,但在 Visual Studio 中不运行(它们尚未在 Visual C++ 11 中实现)。

全局或命名空间作用域中的常量

全局或命名空间作用域中的常量(如果未声明为 extern)具有内部链接。这意味着此类常量可以定义在 h 文件中,并在不同模块中使用,而不会出现重复定义等错误。您可以添加 static 关键字,但这多余。

// const1_include.h
const double x = 3.2;
namespace A
{
    const double y = 5.0; 
}
 
double fx();
double fy();

这是实现 fx()fy() 的 cpp 文件

//const1_include.cpp
#include "const1_include.h"
double fx() { return x;}
double fy() { return A::y;}

这是主 cpp 文件

// const1.cpp
include <iostream>
#include "const1_include.h"
#include <string>
 
void f(const std::string& name, const int& x)
{
    std::cout << name << x << std::endl;
}
 
int main()
{
    f("x: ", fx());
    f("y: ", fy());
    f("x: ", x);
    f("y: ", A::y);    
    return 0;
}

此程序在 Visual Studio 和 GCC 4.7.0 中都能运行。问题在于,与变量相反,常量具有内部链接。

问题在于,当我们将常量用于必须在编译时定义的地方时,例如作为数组定义中数组的大小

double a[N];

此处 N 必须在编译时定义。为了保证满足某些特殊条件。让我们看一些问题。如果定义了一个 double 常量,则不能在数组定义中使用它

const double p = 7.2;
int a[(int)p]; // error!

如果我们定义以下模板函数

template<class T, int N>
int array_size(const T (&a)[N])
{
    return N;
}

它可以用来确定数组的大小

int b[] = {1,2,3};
std::cout << array_size(b) << std::endl;

但它不能用来定义另一个数组

double c[array_size(b)]; // error!

原因是这些值未在编译时定义。

在 C++11 中,可以定义常量、函数和类,以便它们可用于在编译时定义其他对象。一个特殊的关键字 constexpr 用于定义此类结构。通常,在编译时可用的表达式称为 常量表达式。函数和类有一些特殊条件,以便它们可以使用 constexpr 关键字进行定义。在上述情况下,代码可以修改如下(在 GCC 4.7.0 中有效)

constexpr double p = 7.2;
int a[(int)p]; 
template<class T, int N>
constexpr int array_size(const T (&a)[N])
{
    return N;
}
int b[] = {1,2,3};
double c[array_size(b)];

类作用域中的常量

类作用域中定义的静态常量具有外部链接,这意味着必须有一个模块对其进行完全定义。整数静态常量可以在声明中直接指定其值

struct S
{
    static const int n = 25;
    double a[n];
};

这对数组来说效果很好,但如果程序中的任何地方都使用了常量的引用,则该常量应在命名空间级别定义,如下所示

const int S::n;

有时,在意想不到的情况下可能需要引用。请考虑以下程序

#include <iostream>
struct S
{
    static const int n = 25;
    static const int m = 50;
    double a[n];
};
 
int main()
{
    std::cout << std::max(S::m, S::n) << std::endl; // linking error!
    return 0;
}

GCC 4.7.0 将会给出“未定义引用”错误。要解决此问题,应在“int main()”之前添加以下两行

const int S::n;
const int S::m;

另一方面,与其使用静态 int 常量,不如使用枚举类型:这要容易得多,而且不会出现任何意外。请考虑以下程序

#include <iostream>
#include <iomanip>

struct A
{
    enum { n = 27, m = 51, p = -82 };
    enum: long long int { c = 0x12345678ABCDEF03 };
    enum: char { z = 'z' };
    double a[n];
};

void f(const int& k)
{
     std::cout << k << std::endl;
}

void g(int k)
{
     std::cout << k << std::endl;
}

int main()
{    
    std::cout << std::max(A::m, A::n) << std::endl;
    f(A::m);
    f(A::n);
    f(A::p);
    g(A::m);
    g(A::n);
    g(A::p);
    std::cout << "c: " << std::hex << A::c << std::dec << std::endl;
    std::cout << "z: " << A::z << " " << (char)A::z << std::endl;

    return 0;
} 

在此程序中,“常量”的行为都很自然,而且定义它们很容易——无需额外努力。程序将打印

51
51
27
-82
51
27
-82
c: 12345678abcdef03
z: 122 z 

 唯一的小问题是 z 的值。 看看最后一行: 我们  必须显式将该值转换为  char 类型,才能将其打印为字符。默认情况下,它被打印为整数,因为 z 是一个枚举数。 

对于其他类型,情况不同。如果我们使用普通的 const,则不能在声明中指定值,只能在全局命名空间作用域的定义中指定。例如

struct S
{
    static const double p;
};
 
const double S::p = 7.5;

这对于单个模块来说效果很好,但当定义多个模块时,并不总是好的。请考虑由多个模块组成的以下程序

//my_class.h
class A 
{ 
public:        
    static const double c; 
}; 
//----------------------------

//my_class.cpp
#include "my_class.h"
const double A::c = 1000.0; 
//-----------------------------

//my_class2.h
class B
{ 
public:     
    static const double d;
}; 
//-----------------------------

//my_class2.cpp
#include "my_class.h"
#include "my_class2.h"
 
const double B::d = A::c;
//-----------------------------

//static_const_inside_class.cpp
#include <iostream>
#include "my_class.h"
#include "my_class2.h"
 
const double div2 = B::d/10.0;
 
int main()
{    
    std::cout << A::c << std::endl; 
    std::cout << B::d << std::endl;
    std::cout << div2 << std::endl;
    return 0;
}

与大多数程序员的期望相反,程序通常会输出

1000
1000
0

最后一个值通常(不总是,取决于系统)是 0,而不是 100。原因是,在其他模块中初始化了值的静态常量,在 main 执行之前不保证能获得值, 这意味着当定义 div2 时 B::d 的值可能不可用。这可能导致许多错误。

现在,在 C++11 中,constexpr 可以帮助解决这些问题。首先,constexpr 可用于 intdouble 类型,并且它们都可以在类内部进行初始化。这些值保证在编译时可用。因此,您可以安全地使用它们。您可以重写上述模块如下

//my_class.h
class A 
{ 
public:     
    static constexpr double c = 1000.0;
}; 
//--------------------------------------

//my_class.cpp
#include "my_class.h"
 
constexpr double A::c;
//--------------------------------------

//my_class2.h
#include "my_class.h"
class B
{ 
public:     
    static constexpr double d=A::c;
}; 
//---------------------------------------

//my_class2.cpp
#include "my_class2.h"
 
constexpr double B::d;
//-----------------------------------------

//static_constexpr_inside_class.cpp 

#include <iostream>
#include "my_class2.h"
 
constexpr double div2 = B::d / 10.0;
 
int main()
{
    std::cout << A::c << std::endl;
    std::cout << B::d << std::endl;
    std::cout << div2 << std::endl;
    return 0;
}

此程序将根据普遍预期打印

1000
1000
100

Consexpr 函数

普遍的问题是,如果您定义 constexpr 对象(constexpr 常量),与 const 对象(普通常量)相反,您只能使用常量表达式初始化它们,其中不允许使用普通函数,而只能调用 constexpr 函数。constexpr 函数 应满足以下条件

  • 其主体只能有一个语句,该语句应为返回非 void 值的 return(允许一些 assert);
  • 函数的值及其参数(如果有)应为允许作为 constexpr 的类型。

为了编写复杂的算法,您唯一的选择是使用递归。递归深度有一个最小限制,实现应允许该限制:512。

Constexpr 函数可以相互递归,但必须在使用它来定义 constexpr 对象之前完全定义该函数。constexpr 函数可以与非 constexpr 参数(例如,变量)一起调用,但在这种情况下,其值不是常量表达式。您不能定义一个具有与另一个非 constexpr 函数相同名称和参数的 constexpr 函数。

C++11 标准不要求 <cmath> 中的函数是 constexpr,这意味着,作为一般规则,像 sin(x) 和 sqrt(x) 这样的函数不能在常量表达式中使用。但是,在 GCC 4.7.0 中,它们被定义为 contsexpr 函数,这是对标准的扩展。如果,在特定实现中,sqrt 未被定义为 constexpr,您可以定义自己的 constexpr 函数,但必须以不同的方式调用它,例如 Sqrt。这样的定义可能如下所示(我们定义了辅助函数 Abs 和 Sqrt_impl)

constexpr double Abs(double x)
{
    return (x > 0.0 ? x : -x);
}
constexpr double Sqrt_impl(double y, double x1, double x2, double eps)
{
    return (Abs(x1 -x2) < eps ? x2 : Sqrt_impl(y, x2, (x2+y/x2)*0.5, eps));
}
 
constexpr double Sqrt(double y) 
{
    return Sqrt_impl(y, 0, y*0.5 + 1.0, 1e-10);
}

这是另一个示例,显示了 cos(x) 和 sin(x) 的 contsexpr 定义

constexpr double SinCos_impl(double x2, double n, double u)
{
    return (Abs(u) < 1e-10 ? u : u + SinCos_impl(x2, n+2.0, -u*x2/(n * (n + 1))));
}
 
constexpr double Cos(double x);
 
constexpr double Sin(double x)
{
    return (Abs(x) < 0.5 ? SinCos_impl(x*x, 2.0, x) : 2 * Sin(x * 0.5) * Cos(x * 0.5));
}
 
constexpr double Sqr(double x)
{
    return x*x;
}
 
constexpr double Cos(double x)
{
    return (Abs(x) < 0.5 ? SinCos_impl(x*x, 1.0, 1.0) : Sqr(Cos(x * 0.5)) - Sqr(Sin(x * 0.5)));
}

函数形式参数永远不指定为 constexpr。

常量表达式中可使用的类型

C++11 标准定义了所谓的 字面量类型,它们可用于常量表达式。字面量类型
• 算术类型(整数、浮点数、字符类型或 bool 类型);
• 指针类型;
• 字面量类型的引用类型(例如,int& 或 double&);
• 字面量类型的数组;
• 字面量类。


字面量类具有以下属性
• 它没有用户定义的析构函数,并且其所有基类都没有用户定义的析构函数;
• 其非静态数据成员和基类为字面量类型;
• 其所有非静态数据成员都使用常量表达式进行初始化;
• 它满足以下两个条件之一

  1. 它没有用户提供的构造函数,非静态数据成员没有初始化器,没有私有或受保护的非静态数据成员,没有基类,也没有虚函数;
  2. 它至少有一个 constexpr 构造函数或构造函数模板,以及可能的复制和移动构造函数; constexpr 构造函数通常有一个空体,但允许初始化所有类成员。

因此,构造函数可以定义为 constexpr。成员函数可以是 constexpr,如果它们不是虚函数。在常量表达式中,您可以使用指针,但不允许使用 new 访问堆上分配的数据。   

这是一个字面量类及其用法的示例

struct RGB
{
    unsigned char r, g, b;
};
 
constexpr RGB Red{255, 0,0};
constexpr RGB Green{0, 255,0};
constexpr RGB Blue{0, 0,255};

非静态成员对象从不声明为 constexpr。

但如果您想为 RGB 类添加一些操作,可以按如下方式修改代码

struct RGB
{
    unsigned char r, g, b;
    constexpr RGB(unsigned char x, unsigned char y, unsigned char z): r(x),g(y),b(z) {}
};
 
constexpr RGB Red{255, 0,0};
constexpr RGB Green{0, 255,0};
constexpr RGB Blue{0, 0,255};
 
constexpr unsigned char Limit255(unsigned x)
{
      return ( x > 255 ? 255 : x);
}
 
constexpr RGB operator+(const RGB& x, const RGB& y)
{
      return RGB(Limit255(x.r + y.r), Limit255(x.g + y.g), Limit255(x.b + y.b));
}
 
constexpr RGB Yellow  = Red+Green;
constexpr RGB Magenta = Red+Blue;
constexpr RGB Cyan    = Green+Blue;

如何在 Consexpr 类中初始化成员数组

困难在于 std::stringstd::vector 以及其他容器不能在常量表达式中使用。您只能使用字面量类型。让我们定义一个字面量数组类:我们称之为 ConstArray,它是一个固定大小的数组,可在常量表达式中使用。
挑战在于初始化类中的数组成员,该成员取决于参数。您不允许使用非 constexpr 函数或构造函数。让我们先看一个包含三个元素的简单数组示例

struct ConstArray3
{    
    const double a[3];
    constexpr ConstArray3(double a1, double a2, double a3):a{a1,a3,a3} {}
    constexpr int size() { return 3;}
    constexpr double operator[](int i) { return a[i];}    
};

花括号初始化列表有助于实现这一点。如果我们想考虑一个任意数量元素的数组,那么可变参数模板将有所帮助

template <class ElemType, int N>
class ConstArray  
{
    const ElemType a[N];
public:    
    template <class ... T> constexpr ConstArray(T ... p):a {p...} {}
    constexpr int size() { return N;}
    constexpr ElemType operator[](int i) 
 
     { return (i < 0 || i >= N ? throw "ERROR: out of range": a[i]);}    
};

现在我们可以轻松地这样使用它

constexpr ConstArray<double, 3> a{1.0, 2.5, 3.0};
constexpr ConstArray<char, 4>s{'a','b','c','d'};

如何在 Constexpr 类中初始化成员字符串

由于我们不能在常量表达式中使用 std::string,因此我们可以使用 const char*const char[]。让我们考虑第一种选择

class ConstString
{
    const char* s;      
    const int n;  
public:        
    constexpr ConstString(const char* s1, int n1): s(s1),n(n1-1){};    
           
    constexpr char operator[](int j)
    {
        return ( j < 0 || j >= n ? throw "ERROR: ConstString index out of bounds" : s[j]);        
    }
    constexpr int size() { return n; }          
    constexpr const char* c_str() { return s;} 
    operator std::string() const { return std::string(s,n); }  // not a constexpr operator  
}; 

最后一个运算符不是常量表达式,不能在常量表达式中使用。重要的是要知道,constexpr 成员函数不必声明为 const:  const 限定符会自动假定。

创建字符串字面量包装器很方便

template<int N>
constexpr ConstString Str(const char (&s1)[N])
{
    return ConstString(s1, N);
}

这允许我们创建如下的 constexpr 对象

constexpr auto cs(Str("Oranges"));
constexpr auto cs2(Str(" and Apples"));
constexpr auto empty(Str(""));

我们甚至可以创建使用 ConstString 成员的结构

class PersonalData
{
    ConstString name;
    ConstString surname;
public:
    template <int NameLength, int SurnameLength>
    constexpr PersonalData(const char (&name1)[NameLength], const char(&surname1)[SurnameLength]):
                           name(name1,NameLength),surname(surname1,SurnameLength) {}
    constexpr ConstString getName() { return name;}
    constexpr ConstString getSurname() { return surname;}
};
 
template <int NameLength, int SurnameLength>
constexpr PersonalData CreatePersonalData(const char (&name1)[NameLength], const char(&surname1)[SurnameLength])
{
    return PersonalData(name1, surname1);
}
 
constexpr auto JohnSmith = CreatePersonalData("John","Smith");
constexpr auto MaryGreen = CreatePersonalData("Mary","Green");

如果我们想比较 ConstString 值,也是可能的。由于我们必须仅依赖递归函数才能使我们的值保持在常量表达式的范围内(这里我们再次需要定义一个辅助函数)

constexpr int Compare_Impl(const ConstString& s1, const ConstString& s2, int i)
{
    return (i >= s1.size() && i >= s2.size() ? 0 : 
               (i >= s1.size() ? -1 : 
                     (i >= s2.size() ? 1 : Compare_Impl(s1,s2, i+1))));
};
 
constexpr int Compare(const ConstString& s1, const ConstString& s2)
{
    return Compare_Impl(s1,s2,0);
}

也可以创建用户定义的字面量,它允许编写字符串字面量(而不是使用函数 Str),其值将是 ConstString

constexpr ConstString operator "" _S(const char* s, std::size_t n) { return ConstString(s,n+1);} 

这使您可以编写简洁明了的代码

 constexpr auto s1 = "An apple tree"_S; // s1 is of type ConstString       
 std::string s2 = s1; // automatic conversion to std::string is allowed
 constexpr ConstString s3 = s1; // copying is allowed

参考文献

[1] Working Draft, Standard for Programming Language C++

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3376.pdf

© . All rights reserved.