필수 연산
타입의 생성자, 소멸자, 복사 이동 연산은 연관된 집합이다.
같은 논리로 정의하지 않는다면 성능 등의 문제가 생긴다.
어떠한 클래스가 free, delete (리소스 해제) 같은 중요한 작업을 한다면 (소멸자) 해당 클래스에 함수 구성 전체가 필요하다.
아래와 같은 경우 복사, 이동 연산이 일어난다.
1. 할당의 소스: b = a;
2. 객체 초기자: MyClass b(a);
3. 함수 인수: foo(a);
4. 함수 반환값: return obj;
5. 예외: throw obj
할당을 제외한 경우는 복사, 이동 생성자를 사용한다.
기본 구현
일반적인 사용자 정의 생성자를 제외하고, 특수 멤버 함수는 필요하다면 컴파일러가 자동으로 생성한다.
기본 구현 생성을 default 키워드를 통해 명시적으로 할 수도 있다.
class A
{
public:
A(int v1, int v2) : b{v1}, c{v2}
{
b = v1;
c = v2;
}
A(const A&) = default;
private:
int b;
int c;
};
기본 구현 자체를 문법적으로 막고 싶으면 delete키워드를 통해 막을 수 있다.

위의 그림을 보면 삭제된 함수입니다. 를 통해 복사 할당이 허용되지 않음을 볼 수 있다.
일반적으로 필수 연산은 전부 정의하거나 하나도 정의하는 것이 좋다. (0의 법칙)
변환
암묵적 변환
하나의 인자를 받는 생성자가 있다면, 컴파일러가 자동으로 해당 타입을 클래스로 변환할 수 있게 해준다.
class A {
public:
A(int x) { } // 변환 생성자
};
void foo(A a) { }
int main() {
foo(10); // int → A로 암묵적 변환 → A a(10)
}
또한 operator Type() 형태를 통해서 암묵적으로 변환할 수 있다.
그리고 상속관계, 표준 타입 간 암묵적 변환과 혼합 시에 암묵적 변환이 가능하다.
하지만 암묵적 변환이 항상 의도와 같게 동작하는 것은 아니기 때문에 막아야 하는 경우도 있다.
명시적 변환만 가능하게
explicit 키워드를 통해 명시적 변환만 가능하도록 제한할 수 있다.

암묵적 변환이 제한되어, 적절한 생성자가 없다고 나오는 것을 확인할 수 있다.
복사와 이동
복사
복사는 기본적으로 클래스의 멤버 단위로 복사한다.
클래스가 자원핸들인 경우에는 복사가 좋지 않은 결과를 초래할 수 있다.
자원핸들을 복사한다면 2개의 서로 다른 핸들이 같은 자원을 참조하고, 하나의 핸들이 자원을 반환한다면 남은 핸들은 정의되지 않은 동작을 할 확률이 높아진다.
이동
복사의 경우 컨테이너가 클수록 복사 비용이 커진다
참조를 사용하면 함수에 객체를 전달할 때는 복사 비용이 줄어들지만, 지역 객체 참조를 결과로 반환할 수는 없다.
지역 객체는 해당 스코프를 벗어나면 소멸하기 때문에 참조가 의미가 없기 때문이다.
operation+ 같은 경우에는 새로운 객체를 반환하기 때문에 복사보다는 이동이 적합하다고 할 수 있다.
만약에 복사연산을 통해 반환된다면, a = b + c + d + e;와 같은 연산을 할 때 이후에 사용하지 않을 임시객체를 계속해서 반환함을 알 수 있다. (다만 모던 C++ 에서는 이동조차 하지않고 RVO를 사용해 반환한다)
rvalue, &&
&&은 rvalue 참조라는 의미로 사용한다.
rvalue는 lvalue가 아닌 값으로, lvalue는 할당문 왼편에 넣을 수 있는 값 (할당을 받을 수 있는 값) 이라고 생각할 수 있다.
rvalue 참조는 해당 rvalue가 다시 사용되지 않음을 알기 때문에, 복사하지 않고 안전하게 값을 사용할 수 있다.
클래스 내에 T&&타입의 인자를 통해 이동 연산, 이동 할당을 할 수 있다.
일반적인 lvalue는 rvalue 참조에 사용할 수 없기 때문에 std::move()를 통해 강제로 rvalue로 캐스팅할 수 있다.

string 처럼 잘 만들어진 이동 연산을 사용하면 이전의 값은 사용할 수 없음을 확인할 수 있다.
이동 연산은 인수의 값을 제거하기 때문에 const 인자를 받을 수 없다.
자원 관리
내장 포인터를 직접 사용하는 것보다, 자원 핸들을 사용하는 것이 안전하다. unique_ptr은 잘 구현된 자원 핸들의 예시이다.
메모리 외의 락, 소켓, 파일 핸들, 스레드 핸들 등의 자원을 비메모리 자원이라고 부른다.
RAII( Resource acquisition is initialization)를 활용해 자원 사용, 반환을 체계적으로 할 수 있다.
연산자 오버로딩
C++에서는 기본 연산자 오버로딩이 가능하다.
완전 새로운 연산자는 정의할 수 없다.
오버로딩 시에는 전통적 의미를 기반으로 코드를 디자인해야 한다.
사용자 정의 리터럴
C++은 L을 문자열 앞에 붙여 유니코드 문자열을 만들고 문자열 s를 뒤에 붙여 string으로 취급하는 등 리터럴을 지원한다.
사용자 정의 타입도한 이러한 리터럴을 사용할 수 있다.
constexpr complex<double> operator""i(long double arg) {
return {0, arg};
}
조언
1. 객체의 생성, 복사, 이동 소멸을 제어하자.
2. 생성자, 할당, 소멸자를 연관된 집합으로 디자인
3. 필수연산은 모드 정의하거나 모두 정의하지 않는다.
4. default생성자, 할당, 소멸자가 적절하면 컴파일러가 생성하게 한다.
5. 클래스가 포인터 멤버를 포함할 때 소멸자, 복사, 이동을 다시 정의해야하는지 생각하자.
6. 클래스가 사용자 정의 소멸자를 포함할 때는 사용자 정의, deleted 복사, 이동이 필요할 가능성이 높다.
7. 인수가 하나인 생성자는 explicit으로 선언해 암묵적 변환을 막는다.
8. 클래스 멤버에 적절한 default값을 제공할 수 있다면, 제공한다.
9. default값이 타입에 적절하지 않은 경우 재정의 ,복사를 금지한다.
10. 컨테이너는 값으로 반환한다. (RVO)
11. 명시적 std::copy는 사용하지 않는다.
12. 피연산자가 크고 변경하지 않는다면 const & 인수 타입을 사용한다.
13. 자원(resource)을 누수시키지 않아야 한다.
14. 클래스가 자원 핸들을 포함할 때 해당 클래스는 사용자 정의 생성자, 소멸자, non-default 복사 연산이 필요하다.
15. RAII를 통해 메모리, 비메모리 자원 관리
16. 연산자 오버로딩 시에는 전통적 의미를 생각하자.
17. 연산자 오버로딩 시에는 함께 쓰이는 연산자를 같이 정의하자.(==과 != 등)
18. <=>를 non-default로 정의한다면 == 도 정의한다.
19. 표준 라이브러리 컨테이너 디자인을 따른다.
'C, C++ > C++ 여행' 카테고리의 다른 글
| [A Tour of C++] 7장 - 템플릿 (1) | 2025.06.10 |
|---|---|
| [A Tour of C++] 5장 - 클래스 (0) | 2025.05.14 |
| C++ 여행 조언 새기기 4 (0) | 2025.02.22 |
| C++ 여행 조언 새기기 3 (1) | 2025.02.22 |
| C++ 여행 조언 새기기 2장 (0) | 2025.02.16 |