Program Language/C & C++

[C++] Constant Expressions

야곰야곰+책벌레 2022. 1. 6. 17:19
728x90
반응형

C++에서 'constant'는 두 가지 관련 의미를 제공한다.

  • constexpr : 컴파일 타임에 평가
  • const : scope 내 수정 방지

기본적으로 constexpr의 역할은 compile-time 평가를 활성화하고 보장하는 반면 const의 주요 역할은 인터페이스에서 불변성을 지정하는 것이다. 여기서는 compile-time 평가와 관련 있다. 컴파일 시 알려지지 않은 값은 사용할 수 없으며 이에 대한 side effect는 없다. 궁극적으로 constant 표현식은 정수, 실수, 열거자로 시작해야 하며 다음을 수행하는 constexpr 함수와 연산자를 사용하여 이들을 결합하여 값을 생성할 수 있다. 또한 일부 주소는 일부 형태의 상수 표현식에서 사용할 수 있다. (따로 논의)

 

누군가가 리터럴이나 변수에 저장된 값보다 명명된 상수를 원하는 데에는 다양한 이유가 있다.

  1. 명명된 상수를 사용하면 코드를 더 쉽게 이해하고 유지 관리할 수 있다.
  2. 변수는 변경될 수 있다. (그래서 상수보다 reasoning에 더 주의해야 한다.)
  3. 언어에는 배열 크기, 케이스 레이블 및 템플릿 값 인수에 대한 상수 표현식이 필요하다.
  4. 임베디드 시스템 프로그래머는 변경 불가능한 데이터를 읽기 전용 메모리에 넣는 것을 좋아한다.
    읽기 전용 메모리가 동적 메모리보다 적게 소모되고 풍부하기 때문이다. 또한 읽기 전용 메모리의 데이터는 대부분의 시스템 충돌에 영향을 받지 않는다.
  5. 컴파일 시간에 초기화가 수행되면 다중 스레드 시스템에서 해당 개체에 대한 데이터 경합이 있을 수 없다.
  6. 때로는 컴파일 타임에 한 번 평가하는 것이 런타임에 백만 번 수행하는 것보다 훨씬 더 나은 성능을 제공한다.

1, 2, 3와 (부분적으로) 4의 이유는 논리적이다. 성능에 대한 집착 때문에 상수 표현을 사용하지는 않는다. 종종 그 이유는 상수 표현식이 시스템 요구 사항을 보다 직접적으로 나타내기 때문이다.

데이터 항목 정의(변수)의 일부로 constexpr은 compile-time 평가의 필요성을 나타낸다. constexpr의 initializer를 compile-time에 평가할 수 없으면 컴파일에서 오류가 발생한다.

int x1 = 7;
constexpr int x2 = 7;
constexpr int x3 = x1; // error : initializer is not a constant expression
constexpr int x4 = x2; // OK

void f()
{
	constexpr int y3 = x1; // error : initializer is not a constant expression
	constexpr int y4 = x2; // OK
	// ...
}

영리한 컴파일러는 x3의 초기화 프로그램에서 x1의 값이 7 임을 추론할 수 있다. 하지만 컴파일러의 영리함 정도에 의존하지 않는 것을 더 선호한다. 큰 프로그램에서 컴파일 타임에 변수 값을 결정하는 것은 일반적으로 매우 어렵거나 불가능하다.

costant 표현식의 표현력은 대단하다. 정수, 실수 및 열거형 값을 사용할 수 있다. 상태를 수정하지 않는 모든 연산자를 사용할 수 있다. (예: +,?:, [], 하지만 = 와 ++는 제외) constexpr 함수와 리터럴 형식을 사용하여 상당한 수준의 형식 안전성과 표현력을 제공할 수 있다. 이것을 일반적으로 매크로로 수행되는 작업과 비교하는 것은 불공평하다.

 

operator ? 는 const 표현식의 선택 수단이다. 예를 들어 compile-time에 정수 제곱근을 계산할 수 있다.

constexpr int isqrt_helper(int sq, int d, int a)
{
	return sq <= a ? isqrt_helper(sq+d,d+2,a) : d;
}

constexpr int isqrt(int x)
{
	return isqrt_helper(1,3,x)/2 − 1;
}

constexpr int s1 = isqrt(9); // s1 becomes 3
constexpr int s2 = isqrt(1234);

이 기능은 주로 constant 표현식으로 사용되기도 하고 사용하지 않기도 하는 constexpr 함수에서 유용하다.

const

const는 주로 인터페이스를 표현하는 데 사용된다. 그러나 const는 상수 값을 표현하는 데에도 사용할 수 있다.

const int x = 7;
const string s = "asdf";
const int y = sqrt(x);

상수 표현식으로 초기화된 const는 상수 표현식에서 사용할 수 있다. const는 상수 표현식이 아닌 것으로 초기화될 수 있다는 점에서 constexpr과 다르다. 아래의 경우 const를 상수 표현식으로 사용할 수 없다.

constexpr int xx = x; // OK
constexpr string ss = s; // error : s is not a constant expression
constexpr int yy = y; // error : sqr t(x) is not a constant expression

이 오류는 string이 리터럴 유형이 아니고 sqrt()가 constexpr 함수가 아니기 때문이다.

일반적으로 constexpr은 간단한 상수를 정의하는데 const보다 더 나은 선택이지만 constexpr은 C++11의 새로운 기능이므로 이전 코드에서는 const를 사용하는 경향이 있다. 많은 경우 열거자(enum)는 const의 도 다른 대안이다.

literal type

상수 표현식에는 충분히 간단한 사용자 정의 유형을 사용할 수 있다.

struct Point {
	int x,y,z;
	constexpr Point up(int d) { return {x,y,z+d}; }
	constexpr Point move(int dx, int dy) { return {x+dx,y+dy}; }
	// ...
};

constexpr 생성자가 있는 클래스를 리터럴 유형이라고 한다. constexpr이 될 만큼 간단하려면 생성자의 본문이 비어 있어야 하고 모든 멤버는 잠재적으로 상수 표현식으로 초기화되어야 한다.

constexpr Point origo {0,0};
constexpr int z = origo.x;
constexpr Point a[] = {
	origo, Point{1,1}, Point{2,2}, origo.move(3,3)
};
constexpr int x = a[1].x; // x becomes 1
constexpr Point xy{0,sqrt(2)}; // error : sqr t(2) is not a constant expression

constexpr 배열을 가질 수 있고 배열 요소와 개체 멤버에 액세스 할 수도 있다.

당연히, 리터럴 유형의 인수를 취하도록 constexpr 함수를 정의할 수 있다.

constexpr int square(int x)
{
	return x∗x;
}

constexpr int radial_distance(Point p)
{
	return isqrt(square(p.x)+square(p.y)+square(p.z));
}

constexpr Point p1 {10,20,30}; // the default constructor is constexpr
constexpr p2 {p1.up(20)}; // Point::up() is constexpr
constexpr int dist = radial_distance(p2);

constexpr 부동 소수점 제곱근 함수가 없기 때문에 double 대신 int를 사용했다. 멤버 함수의 경우 constexpr은 const를 의미하므로 다음과 같이 작성할 필요가 없다.

constexpr Point move(int dx, int dy) const { return {x+dx,y+dy}; }

reference argument

constexpr로 작업할 때 기억해야 할 핵심은 constexpr이 모두 값에 관한 것이라는 것이다. 여기에는 값이나 부작용을 변경할 수 있는 객체가 없다. constexpr은 축소된 compile-time 함수형 프로그래밍 언어를 제공한다. 즉, constexpr()이 참조를 처리할 수 없다고 추측할 수 있지만, const 참조가 값을 참조하므로 사용할 수 있기 때문에 부분적으로만 사실이다. 일반적인 complex<T>를 표준 라이브러리 complex<double>로 특화하는 것을 고려해 보자.

template<> class complex<double> {
public:
	constexpr complex(double re = 0.0, double im = 0.0);
	constexpr complex(const complex<float>&);
	explicit constexpr complex(const complex<long double>&);
    
	constexpr double real(); // read the real part
	void real(double); // set the real part
	constexpr double imag(); // read the imaginary par t
	void imag(double); // set the imaginary par t
    
	complex<double>& operator= (double);
	complex<double>& operator+=(double);
	// ...
};

분명히 객체를 수정하는 = 및 +와 같은 작업은 constexpr일 수 없다. 반대로 read() 및 imag()와 같이 단순히 객체를 읽는 작업은 constexpr일 수 있으며 상수 표현식이 주어지면 compile-time에서 평가될 수 있다. 흥미로운 멤버는 다른 복합 유형의 템플릿 생성자다.

constexpr complex<float> z1 {1,2}; // note: <float> not <double>
constexpr double re = z1.real();
constexpr double im = z1.imag();
constexpr complex<double> z2 {re,im}; // z2 becomes a copy of z1
constexpr complex<double> z3 {z1}; // z3 becomes a copy of z1

컴파일러는 참조(const complex<float>&)가 상수 값을 참조한다는 것을 컴파일러가 인식하고 해당 값을 사용하기 때문에 복사 생성자가 작동한다.

리터럴 형식을 사용하면 형식이 풍부한 compile-time 프로그래밍이 가능하다. 전통적으로 C++ compile-time 평가는 정수 값(함수를 제외)을 사용하는 것으로 제한되었다. 그 결과 사람들은 모든 종류의 정보를 정수로 인코딩하기 때문에 불필요하게 복잡하고 오류가 발생하기 쉬운 코드가 생성되었다. 템플릿 메타 프로그래밍의 일부 사용이 그 예다. 

address

전역 변수와 같이 정적으로 할당된 객체의 주소는 상수다. 그러나 그 값은 컴파일러가 아닌 링커에 의해 할당되므로 컴파일러는 이러한 주소 상수의 값을 알 수 없다. 이는 포인터 및 참조 유형의 상수 표현식 범위를 제한한다.

constexpr const char∗ p1 = "asdf";
constexpr const char∗ p2 = p1; // OK
constexpr const char∗ p2 = p1+2; // error : the compiler does not know the value of p1
constexpr char c = p1[2]; // OK, c==’d’; the compiler knows the value pointed to by p1

 

728x90
반응형

'Program Language > C & C++' 카테고리의 다른 글

[C++] 명시적 유형 변환  (0) 2022.01.12
[C++] lambda expression  (0) 2022.01.07
[C++] token 요약  (0) 2022.01.06
[C++] operator 요약  (0) 2022.01.06
[C++] 계산기 만들기 (5) the driver  (0) 2022.01.06