
변환 생성자
암시적 타입 변환이란 컴파일러가 한 타입의 값을 다른 타입의 값으로 암묵적으로 변환하는걸 의미한다, 이때 가능한 변환이 있어야 가능하다
예를들면 다음과 같다
void Foo(double d);
Foo(10); //int -> double 암시적 변환, 값 전달이기 때문에 복사 초기화
그렇다면 다음 코드는 어떨까?
class Knight
{
public:
Knight(int InHp) : hp{ InHp } //변환 생성자
{
}
int GetHp() { return hp; }
private:
int hp{};
};
void Foo(Knight InKnight)
{
std::cout << InKnight.GetHp() << '\n';
}
int main() {
Foo(10);
return 0;
}
Foo()의 매개변수는 Knight 클래스 타입인데 Foo(10);이 그대로 통과하는걸 볼 수 있다
기본 타입끼리는 숫자 변환에 의해 암시적 변환이 가능한데, 숫자 10과 클래스 타입 Knight는 어떻게 변환이 된걸까?
이럴때 바로 변환 생성자를 사용하게 된다
컴파일러는 이러한 불가능한 변환에 있어서 변환을 할 수 있는 함수를 프로그래머가 정의했는지 확인한다
Foo(10)은 Knight(int InHp)생성자를 사용하여 복사초기화 된다 (값 타입), C++17 이전에는 Knight(int)생성자를 사용하여 임시 Knight 객체로 암시적 변환되고 이 임시객체는 매개변수 InKnight로 복사 생성되었다, 하지만 C++17 이후에는 불필요한 복사가 생략되기 때문에 복사 생성자 호출이 되지 않는다
변환 생성자는 단 하나의 사용자 정의 변환만 적용될 수 있다
다음 코드를 보면서 이해해보자
class Employee
{
private:
std::string m_name{};
public:
Employee(std::string_view name) : m_name{ name } {}
const std::string& getName() const { return m_name; }
};
void printEmployee(Employee e) // Employee 매개변수를 가짐
{
std::cout << e.getName();
}
int main() {
printEmployee("Joe"); // 문자열 리터럴 인수를 제공하고 있음
return 0;
}
위 코드는 정상적으로 작동하지 않는다, 왜냐하면 두개의 변환이 필요하기 때문이다
(C-style 문자열 -> std::string_view -> Employee)
코드를 작동시키려면 string_view literal을 사용하여 변환을 줄이거나 명시적으로 Employee객체를 생성하는 방법이 있다
#include <string_view> //std::literals사용을 위한 헤더
int main()
{
using namespace std::literals;
printEmployee("Kelvin"sv);
return 0;
}
printEmployee(Employee{"Kelvin"});
explicit
하지만 이러한 타입 변환 생성자를 사용하고 싶지 않다면 어떻게 해야할까? 바로 explicit 키워드를 사용하면 된다
class Knight
{
public:
explicit Knight(int InHp) : hp{ InHp } {} //explicit
int GetHp() { return hp; }
private:
int hp{};
};
void Foo(Knight InKnight)
{
std::cout << InKnight.GetHp() << '\n';
}
int main()
{
Knight K1{ 100 };
Foo(100); //compile error
return 0;
}
explicit으로 컴파일러에게 해당 생성자가 암시적 변환을 하는 타입 변환 생성자로 사용되면 안된다고 알릴 수 있다, 따라서 Foo(100)으로 함수 호출이 실패한다
explicit 키워드는 선언부에만 사용한다 (정의에서는 사용하지 않음)
explicit은 결국 변환 생성자로 사용되면 안된다는 의미이기 때문에 복사 초기화, 복사 리스트 초기화에 사용할 수 없다, 하지만 직접 초기화, 리스트 초기화에는 사용이 가능하다
explicit Knight(int InHp) : hp{ InHp } {}
Knight K1 = 100; //error
Knight K1 = { 100 } //error
Knight K1{ 100 } //ok
Knight K1(100); //ok
explicit Knight(int InHp) : hp{ InHp }{}
void Foo(Knight InKnight);
Foo(Knight{ 100 }); //ok 명시적으로 Knight 클래스 타입 임시 객체를 생성해서 넘긴것이기 때문에 가능 (변환이 필요 없음)
Foo(static_cast<Knight>(100)); //ok static_cast는 explicit 생성자를 사용함
함수의 값 타입 반환에서도 마찬가지로 암시적 변환이 있는 반환이라면 explicit 생성자가 있을경우 컴파일 에러가 발생한다
class Knight
{
public:
explicit Knight() {}
explicit Knight(int InHp) : hp{ InHp } {}
int GetHp() { return hp; }
private:
int hp{};
};
Knight Foo()
{
return { 100 }; //암시적 변환이 일어나야 하기 때문에 explicit 생성자 사용이 불가능함
}
Knight Foo()
{
return Knight{}; //ok
}
기본적으로 매개변수가 하나인 생성자는 explicit으로 만드는걸 권장한다, 이때 매개변수를 여러개를 가지지만 기본값을 가진다면 마찬가지로 explicit으로 만드는걸 권장한다
결국 변환 생성자를 explicit으로 만들게 되면 해당 생성자를 암시적 변환에 사용하는 것을 허용하지 않는다
하지만 생성된 객체가 인자 값과 의미적으로 동등하거나 변환의 성능이 좋을때는 non-explicit도 고려해볼만 하다
예를들면 std::string과 std::string_view가 있다
std::string_view는 C-style 문자열을 인자로 받는 생성자가 explicit이 아니다 왜냐하면 C-style문자열 그대로를 std::string_view로 처리되는것이 전혀 이상하지 않기 때문이다 (의미적으로도 같고)
하지만 std::string_view를 받는 std::string의 생성자는 explicit이다, 의미적으로는 같지만 std::string으로 변환하는 비용이 크기 때문이다
constexpr 집합체와 클래스
constexpr 함수는 컴파일 타임 혹은 런타임에 평가될 수 있는 함수이다
constexpr int greater(int a, int b)
{
return (a > b) ? a : b;
}
int main()
{
std::cout << greater(10, 20) << '\n'; //compile time or runtime에 평가될 수 있다
constexpr int a{ greater(10, 20) }; //constexpr변수이기 때문에 반드시 compile time에 평가되어야 한다
}
멤버 함수에도 마찬가지로 constexpr 키워드를 사용하여 constexpr 함수를 만들 수 있다 (컴파일 타임 혹은 런타임에 평가될 수 있다)
struct Foo
{
int a{};
int b{};
constexpr int greater() const
{
return (a > b) ? a : b;
}
};
Foo f{ 10, 20 };
std::cout << f.greater() << '\n'; //런타임에 평가
constexpr int a{ f.greater() }; //error
하지만 멤버 함수만 constexpr로 만든다고 해서 컴파일 타임에 평가될 수 있는것이 아니다
왜냐하면 집합체 변수가 constexpr이 아니기 때문에 f.greater()는 상수 표현식이 아니다

따라서 집합체 변수도 constexpr로 만들어줘야 한다
constexpr Foo f{ 10, 20 };
constexpr int a{ f.greater() }; //ok
집합체는 암시적으로 constexpr을 지원한다
그렇다면 아래와 같은 경우에는 어떨까?
class Foo
{
private:
int a{};
int b{};
public:
Foo(int InA, int InB) : a{ InA }, b{ InB } {}
constexpr int greater() const
{
return (a > b) ? a : b;
}
};
int main()
{
constexpr Foo f{10, 20}; //error
std::cout << f.greater() << std::endl;
constexpr int a{ f.greater() }; //error
return 0;
}
private: 멤버 데이터를 가지고 생성자를 가지기 때문에 Foo class는 비집합체이다
위 코드는 Foo가 literal type이 아니라는 컴파일 에러를 발생시킨다, 그렇다면 literal type이란 무엇일까?
literal type이란 상수 표현식 내에서 객체를 생성하는것이 가능한 모든 타입을 의미한다 하지만 Foo는 생성자가 constexpr이 아니기 때문에 literal type이 아니라서 constexpr로 객체 생성이 불가능하다 (상수표현식에서 생성자 호출이 불가능함)
literal, literal type
literal이란 소스코드에 삽입되는 constexpr값이고 literal type은 constexpr값의 타입으로 사용될 수 있는 타입을 의미한다
literal type에는 다음과 같은 타입이 존재한다
따라서 위의 Foo는 constexpr 생성자가 없는 비집합체이기 때문에 literal type이 아니어서 constexpr 상수 표현식 내에서 객체를 생성할 수 없었던 것이다
(생성자가 constexpr이 아니라면 컴파일 타임에 생성될 수 없는 객체이기 때문)
단순히 생성자를 constexpr로 만들면 위의 코드도 잘 동작한다
class Foo
{
private:
int a{};
int b{};
public:
constexpr Foo(int InA, int InB) : a{ InA }, b{ InB } {}
constexpr int greater() const
{
return (a > b) ? a : b;
}
};
int main()
{
constexpr Foo f{10, 20}; //ok
std::cout << f.greater() << std::endl;
constexpr int a{ f.greater() }; //ok
return 0;
}
class타입 객체가 컴파일 타임에 생성될 수 있게 하려면 constexpr 생성자를 사용해야 한다
class Foo
{
private:
int a{};
int b{};
public:
constexpr Foo(int InA, int InB) : a{ InA }, b{ InB } {}
constexpr int greater() const
{
return (a > b) ? a : b;
}
};
constexpr int init()
{
Foo temp{ 10,20 }; //non-constexpr, non-const
return temp.greater();
}
int main()
{
constexpr int a{ init() };
return 0;
}
위의 코드로 정리해보면 a가 constexpr이기 때문에 컴파일 타임에 평가되어야 한다, 따라서 init()도 constexpr함수여야 한다
init() 내부의 temp또한 컴파일 타임에 생성되어야 하기 때문에 Foo클래스의 생성자 중 constexpr 생성자를 호출하고 constexpr 함수인 greater()를 컴파일타임에 호출하여 값을 return하는 과정이 된다
C++11에서는 non-static constexpr 멤버 함수는 생성자를 제외하고 암시적으로 const였지만 C++14부터는 constexpr 멤버 함수는 암시적으로 const가 아니다, 따라서 const로 하고 싶다면 명시적으로 const를 붙여야 한다
그렇기 때문에 명시적으로 const를 붙히지 않는다면 constexpr 멤버 함수에서 멤버 데이터를 변경할 수 있다 (컴파일 타임에 평가될 때도 마찬가지)
class Foo
{
private:
int a{};
int b{};
public:
constexpr Foo(int InA, int InB) : a{ InA }, b{ InB } {}
constexpr int greater()
{
a = 100; //constexpr이고 const가 아니기때문에 가능
return (a > b) ? a : b;
}
};
int main()
{
constexpr Foo f{ 10, 20 };
f.greater(); //error, constexpr 객체에서 non-const함수 호출이 불가능
}
class Foo
{
private:
int a{};
int b{};
public:
constexpr Foo(int InA, int InB) : a{ InA }, b{ InB } {}
constexpr int greater() const
{
a = 100; //error, constexpr이고 const 함수이기 때문에 에러 발생
return (a > b) ? a : b;
}
};
int main()
{
constexpr Foo f{ 10, 20 };
f.greater(); //호출 가능
}
constexpr은 const와 같지 않다, constexpr은 단순히 컴파일러가 컴파일 타임에 평가할 수 있는것을 의미한다
constexpr const int& foo() const { return a; }
constexpr은 해당 함수가 컴파일 타임에 평가될 수 있음을 알린다, 두번째 const는 반환형이 const int&임을 나타내고, 세번째 const는 해당 함수가 const함수임을 나타내어 const객체에 의해 호출될 수 있음을 알린다