C++11~20 constant expression

Posted by dcace on Thu, 28 Oct 2021 06:20:18 +0200

  C++98 Era

The C++98 compiler has a special preference for int constants because they are one of the few things it can directly recognize. Because of this limited capability, the compiler can pre determine the size of the array:

		TEST_METHOD(TestConstVar)
		{
			//int n = 3;
			const int n = 3;
			int a[n] = { 0 };
			Assert::AreEqual(size_t(3), _countof(a));

			const int m = n * 3;
			int b[m] = { 0 };
			Assert::AreEqual(size_t(9), _countof(b));
		}

The concept of "constant folding" is also introduced, that is, the compiler will automatically replace all references to const int variables with constants:

		TEST_METHOD(TestConstVarFold)
		{
			const int a = 10;
			int b = 2 * a;
			int* p = (int*)&a;
			*p = 100;

			// No constant folding?
			Assert::AreEqual(100, a);
			Assert::AreEqual(20, b);
			Assert::AreEqual(100, *p);
		}

We don't have to worry about whether a here is 10 or 100, which depends entirely on the implementation of the compiler. In actual work, whoever wants to write such code will be dragged out and killed.

C++11 era

constexpr value

The limited IQ of C++98 compiler for constants is really worrying. C++11 simply introduces a new keyword constexpr so that the compiler can do more.

		TEST_METHOD(TestConstExprVar)
		{
			constexpr int n = 3;
			int a[n] = { 0 };
			Assert::AreEqual(size_t(3), _countof(a));

			constexpr int m = n * 3;
			int b[m] = { 0 };
			Assert::AreEqual(size_t(9), _countof(b));
		}

Does constexpr look like const? But in fact, you can understand constexpr as a real compile time constant, and const is actually a run-time constant. In the past, the reason why constxpr worked at compile time was a guest act of last resort.

constexpr function

Of course, if constexpr only has this effect, it will never be introduced as a new keyword. More importantly, since the compile time already knows that constexpr represents something that can be run at compile time, why can't it modify functions? Make functions that can only be called at run time work at compile time:

		static constexpr int size()
		{
			return 3;
		}

		static constexpr int sqrt(int n)
		{
			return n * n;
		}

		static constexpr int sum(int n)
		{
			return n > 0 ? n + sum(n - 1) : 0;
		}
		TEST_METHOD(TestConstExprFunc)
		{
			int a[size()] = { 0 };
			Assert::AreEqual(size_t(3), _countof(a));

			int b[sqrt(3)] = { 0 };
			Assert::AreEqual(size_t(9), _countof(b));

			int c[sum(3)] = { 0 };
			Assert::AreEqual(size_t(6), _countof(c));
		}

Of course, in the C++11 phase, this constexpr function has many limitations:

  1. Function must return a value, not void
  2. Function body can only have one return statement
  3. Function must be defined before calling
  4. Functions must be declared with constexpr

Floating-point constants

Although there are some limitations, it is also a function after all, so it is very simple to realize the floating-point constant that is a headache during the compilation of C++98:

		static constexpr double pi()
		{
			return 3.1415926535897;
		}

		TEST_METHOD(TestConstExprDouble)
		{
			int a[(int)pi()] = { 0 };
			Assert::AreEqual(size_t(3), _countof(a));
		}

constexpr class

A major feature of C + + is object-oriented. Since constexpr can modify functions, why not modify member functions?

		class N
		{
		private:
			int m_n;

		public:
			constexpr N(int n = 0)
				:m_n(n)
			{
			}

			constexpr int getN() const
			{
				return m_n;
			}
		};

		TEST_METHOD(TestConstExprConstruct)
		{
			constexpr N n(3);
			int a[n.getN()] = { 0 };
			Assert::AreEqual(size_t(3), _countof(a));
		}

C++14 Era

Constexpr of C++11 is very good and powerful. But the most critical thing is that there are too many restrictions on constexpr functions. So C++14 began to untie it:

		static constexpr int abs(int n)
		{
			if (n > 0)
			{
				return n;
			}
			else
			{
				return -n;
			}
		}

		static constexpr int sumFor(int n)
		{
			int s = 0;
			for (int i = 1; i <= n; i++)
			{
				s += i;
			}

			return s;
		}

		static constexpr int next(int n)
		{
			return ++n;
		}

		TEST_METHOD(TestConstExprFunc14)
		{
			int a[abs(-3)] = { 0 };
			Assert::AreEqual(size_t(3), _countof(a));

			int b[sumFor(3)] = { 0 };
			Assert::AreEqual(size_t(6), _countof(b));

			int c[next(3)] = { 0 };
			Assert::AreEqual(size_t(4), _countof(c));
		}

Basically, this is basically a real function, which is no longer limited to one line of code:

  1. You can use branch control statements
  2. You can use the loop control statement
  3. You can modify variables with the same life cycle and constant expressions, so even expressions such as + + n can be supported

Even if the function must return a value, the restriction that it cannot be void has been removed, so functions such as setN can be written, but this is not very common.

C++17 Era

C++17 further extends the scope of constexpr to lambda expressions:

		static constexpr int lambda(int n)
		{
			return [](int n) { return ++n; }(n);
		}

		TEST_METHOD(TestConstExprLambda)
		{
			int a[lambda(3)] = { 0 };
			Assert::AreEqual(size_t(4), _countof(a));
		}

In order to adapt a function to more situations, C++17 also extends its black hand to the if statement and introduces the so-called "if constexpr":

		template<typename T>
		static bool is_same_value(T a, T b)
		{
			if constexpr (std::is_same<T, double>::value)
			{
				if (std::abs(a - b) < 0.0001)
				{
					return true;
				}
				else
				{
					return false;
				}
			}
			else
			{
				return a == b;
			}
		}
		
		TEST_METHOD(TestConstExprIf)
		{
			Assert::AreEqual(false, is_same_value(5.6, 5.11));
			Assert::AreEqual(true, is_same_value(5.6, 5.60000001));
			Assert::AreEqual(true, is_same_value(5, 5));
		}

In the past, similar code needed a template function and a specialization function. Now one function is done. It's good.

C++20 Era

Not surprisingly, C++20 continues to extend its black hand to more places

constexpr and exception:

		static constexpr int funcTry(int n)
		{
			try
			{
				if (n % 2 == 0)
				{
					return n / 2;
				}
				else
				{
					return n;
				}
			}
			catch (...)
			{
				return 3;
			}
		}

		TEST_METHOD(TestConstExprTry)
		{
			int a[funcTry(6)] = { 0 };
			Assert::AreEqual(size_t(3), _countof(a));

			int b[funcTry(3)] = { 0 };
			Assert::AreEqual(size_t(3), _countof(b));
		}

constexpr and union:

		union F
		{
			int i;
			double f;
		};

		static constexpr int funcUnion(int n)
		{
			F f;
			f.i = 3;
			f.f = 3.14;

			return n;
		}

		TEST_METHOD(TestConstExprUnion)
		{
			int a[funcUnion(3)] = { 0 };
			Assert::AreEqual(size_t(3), _countof(a));
		}

constexpr and virtual functions

  This is a little too much. I don't know how much practical use it is. It's a little.

Immediate function

The function decorated with consteval indicates that it can be executed immediately during compilation. If it cannot be executed, an error will be reported.

		static consteval int sqr(int n)
		{
			return n * n;
		}

		TEST_METHOD(TestConstEval)
		{
			int a[sqr(3)] = { 0 };
			Assert::AreEqual(size_t(9), _countof(a));
		}

Perceptual constant environment

  This is interesting. If you can sense whether it is a constant environment, you can let a function give the implementation at compile time and run time respectively. The method is to use std::is_constant_evaluated():

		static constexpr double power(double b, int n)
		{
			if (std::is_constant_evaluated() && n >= 0)
			{
				double r = 1.0, p = b;
				unsigned u = unsigned(n);
				while (u != 0)
				{
					if (u & 1) r *= p;
					u /= 2;
					p *= p;
				}

				return r;
			}
			else
			{
				return std::pow(b, double(n));
			}
		}

		TEST_METHOD(TestConstEvaluated)
		{
			constexpr double p = power(3, 2);
			Assert::AreEqual(9.0, p, 0.001);
			int m = 2;
			Assert::AreEqual(9.0, power(3, m), 0.001);
		}

reference material

  • Analysis of core features of modern C + + language

Topics: C++ Back-end