class Foo {
friend class Bar;
// ...
};
#include <cstddef>
#include "SpreadsheetCell.h"
class Spreadsheet
{
public:
Spreadsheet(size_t width, size_t height);
void setCellAt(size_t x, size_ y, const SpreadsheetCell& cell);
SpreadsheetCell& getCellAt(size_t x, size_t y);
private:
bool inRange(size_t value, size_t upper) const;
size_t mWidth = 0;
size_t mHeight = 0;
SpreadsheetCell** mCells = nullptr;
};
Spreadsheet::Spreadsheet(size_t width, size_t height)
: mWidth(width), mHeight(height)
{
mCells = new SpreadsheetCell*[mWidth];
for (size_t i = 0; i < mWidth; i++) {
mCells[i] = new SpreadsheetCell[mHeight];
}
}
void Spreadsheet::setCellAt(int x, int y, const SpreadsheetCell& cell) {
if (!inRange(x, mWidth) || !inRange(y, mHeight)) {
throw std::out_of_range("");
}
mCells[x][y] = cell;
}
SpreadsheetCell& Spreadsheet::getCellAt(int x, int y) {
iif (!inRange(x, mWidth) || !inRange(y, mHeight)) {
throw std::out_of_range("");
}
return mCells[x][y];
}
void verifyCoordinate(size_t x, size_ y) const;
void Spreadsheetcell::verifyCoordinate(size_t x, size_t y) const {
if (x >= mWidth || y >= mHeight) {
throw std::out_of_range("");
}
}
// 수정 후 getCellAt, setCellAt 메서드
void Spreadsheet::setCellAt(size_t x, size_t y, const SpreadsheetCell& cell) {
verifyCoordinate(x, y);
mCells[x][y] = cell;
}
void Spreadsheet::getCellAt(size_t x, size_t y, const SpreadsheetCell& cell) {
verifyCoordinate(x, y);
return mCells[x][y];
}
// Spreadsheet에 소멸자 선언
class Spreadsheet {
public:
Spreadsheet(size_t width, size_t height);
~Spreadsheet();
// 생략
};
// Spreadsheet 클래스의 소멸자 구현
Spreadsheet::~Spreadsheet() {
for (size_t i = 0; i < mWidth; i++) {
delete [] mCells[i];
}
delete [] mCells;
mCells = nullptr;
}
#include "Spreadsheet.h"
void printSpreadsheet(Spreadsheet s){
// 생략
}
int main() {
Spreadsheet s1(4, 3);
printSpreadsheet(s1);
return 0;
}
Spreadsheet s1(2,2), s2(4,3);
s1 = s2;
// 복제 생성자 생성
class Spreadsheet
{
public:
Spreadsheet(const Spreadsheet& src);
// 생략
};
// 복제 생성자 정의
Spreadsheet::Spreadsheet(const Spreadsheet& src)
: Spreadsheet(src.mWidth, src.mHeight)
{
for (size_t i = 0; i < mWidth; i++) {
for (size_t j = 0; j < mHeight; j++) {
mCells[i][j] = src.mCells[i][j];
}
}
}
// 대입 연산자 선언
class Spreadsheet
{
public:
Spreadsheet& operator=(const Spreadsheet& rhs);
// 생략
};
// 대입 연산자 단순 구현
Spreadsheet& Spreadsheet::operator=(const Spreadsheet& rhs)
{
// 자기 자신인지 확인
if (this == &rhs) {
return *this;
}
// 기존 메모리 해제
for (size_t i = 0; i < mWidth; i++) {
delete [] mCells[i];
}
delete[] mCells;
mCells = nullptr;
// 메모리를 새로 할당.
mWidth = rhs.mWidth;
mHeight = rhs.mHeight;
mCells = new Spreadsheetcell*[mWidth];
for (size_t i = 0; i < mWidth; i ++) {
mCells[i] = new SpreadsheetCell[mHeight]; // 익셉션 발생!
}
// 데이터 복제
for (size_t i = 0; i < mWidth; i++) {
for (size_t j = 0; j < mHeight; j++) {
mCells[i][j] = rhs.mCells[i][j];
}
}
return *this;
}
만약 위의 코드에서 익셉션이 발생했다고 하면 문제가 생기게 된다.
데이터 멤버 mWidth, mHeight는 일정크기를 갖고 있지만 실제로는 mCells 데이터 멤버에 필요한 만큼의 메모리를 갖고있지 않는다.
문제가 생기지 않도록 복제 후 맞바꾸기 패턴을 적용하여 해결한다.
Spreadsheet 클래스에 대입 연산자와 swap() 함수를 추가함.
class Spreadsheet
{
public:
Spreadsheet& operator=(const Spreadsheet& rhs);
// 비 멤버 함수로, 예외가 일어나지 않도록 noexcept
friend void swap(Spreadsheet& first, Spreadsheet& second) noexcept;
}
// swap 구현
void swap(Spreadsheet& first, Spreadsheet& second) noexcept
{
using std::swap;
swap(first.mWidth, second.mWidth);
swap(first.mHeight, second.mHeight);
swap(first.mCells, second.mCells);
}
// swap을 사용한 대입 연산자 구현
Spreadsheet& Spreadsheet::operator=(const Spreadsheet& rhs)
{
// 자기 자신인지 확인
if (this == &rhs) {
return *this;
}
Spreadsheet temp(rhs); // 임시 인스턴스에서 처리
swap(*this, temp); // 익셉션 발생하지 않는 연산으로 작업 처리
return *this;
}
정리하면
- 임시 복제본을 만든다.
- swap() 함수를 이용하여 현 객체를 임시 복제본으로 교체
- 임시 객체 제거. 원본 객체가 남음
class Spreadsheet
{
public:
Spreadsheet(size_t width, size_t height);
Spreadsheet(const Spreadsheet& src) = delete;
~Spreadsheet();
Spreadsheet& operator=(const Spreadsheet& rhs) = delete;
// 생략
};
int a = 4 * 2; // a : 좌측값, 4 * 2 : 우측값(임시값)
// 좌측값 레퍼런스 매개변수
void handleMessage(std::string& message) {
cout << "handleMessage with lvalue reference: " << message << endl;
}
// 우측값 레퍼런스 매개변수
void handleMessage(std::string&& message) {
cout << "handleMessage with rvalue reference: " << message << endl;
}
// 이름 있는 변수를 인수로 전달하여 호출
std::string a = "Hello ";
std::string b = "World";
handleMessage(a); // handleMessage(string& value) 호출
handleMessage(a + b); // handleMessage(string&& value) 호출
handleMessage("Hello World"); // handleMessage(string&& value) 호출
// 좌측값을 우측값으로 캐스팅하는 std::move()를 사용하여 컴파일러가 우측값 레퍼런스 버전의 handleMessage를 호출함.
handleMessage(std::move(b)); // handleMessage(string&& value) 호출
class Spreadsheet
{
public:
Spreadsheet(Spreadsheet&& src) noexcept; // 이동 생성자
Spreadsheet& operator=(Spreadsheet&& rhs) noexcept; // 이동 대입 연산자
// 생략
private:
void cleanup() noexcept;
void moveFrom(Spreadsheet& src) noexcept;
// 생략
};
// 구현
void Spreadsheet::cleanup()noexcept {
for (size_t i = 0; i < mWidth; i++) {
delete [] mCells[i];
}
delete [] mCells;
mCells = nullptr;
mWidth = mHeight = 0;
}
void Spreadsheet::moveFrom(Spreadsheet& src) noexcept {
// 데이터의 얕은 복제
mWidth = src.mWidth;
mHeight = src.mHeight;
mCells = src.mCells;
// 소유권 이전으로 소스 객체 리셋
src.mWidth = 0;
src.mHeight = 0;
src.mCells = nullptr;
}
// 이동 생성자
Spreadsheet::Spreadsheet(Spreadsheet&& src) noexcept {
moveFrom(src);
}
// 이동 대입 연산자
Spreadsheet& Spreadsheet::operator=(Spreadsheet&& rhs) noexcept {
// 자기 자신을 대입하는지 확인
if (this == &rhs) return *this;
// 예전 메모리 해제
cleanup();
moveFrom(rhs);
return *this;
}
객체 데이터 멤버 이동하기
- moveFrom() 메서드는 데이터 멤버 세 개를 직접 대입
void Spreadsheet::moveFrom(Spreadsheet& src) noexcept {
// 객체 데이터 멤버 이동
mName = std::move(src.mName);
// 이동 대상:
// 데이터에 대한 얕은 복제
mWidth = src.mWidth;
mHeight = src.mHeight;
mCells = src.mCells;
// 소유권이 이전되어 원본 객체를 초기화
src.mWidth = 0;
src.mHeight = 0;
src.mCells = nullptr;
}
swap() 함수로 구현한 이동 생성자와 이동 대입 연산자
- 이동 생성자와 이동 대입 연산자를 디폴트 생성자와 swap() 함수로 구현
// Spreadsheet의 클래스에 디폴트 생성자 추가
class Spreadsheet
{
private:
Spreadsheet() = default;
// 생략
};
// cleanup(), moveForm() 메서드 삭제
// cleanup() 메서드의 코드를 소멸자로 이동
// 이동 생성자와 이동 대입 연산자 수정
Spreadsheet::Spreadsheet(Spreadsheet&& src) noexcept
: Spreadsheet()
{
swap(*this, src);
}
Spreadsheet& Spreadsheet::operator=(Spreadsheet&& rhs) noexcept
{
Spreadsheet temp(std::move(rhs));
swap(*this, temp);
return *this;
}
Spreadsheet createObject() {
return Spreadsheet(3, 2);
}
int main()
{
vector<Spreadsheet> vec;
for (int i = 0; i < 2; ++i) {
cout << "Iteration " << i << endl;
vec.push_back(Spreadsheet(100, 100));
cout << endl;
}
Spreadsheet s(2, 3);
s = createObject();
Spreadsheet s2(5, 6);
s2= s;
return 0;
}
// 실행 결과
// Iteration 0
// Normal constructor (1)
// Move constructor (2)
// Iteration 1
// Normal constructor (3)
// Move constructor (4)
// Move constructor (5)
// Normal constructor (6)
// Normal constructor (7)
// Move assignment operator (8)
// Normal constructor (9)
// Copy assignment operator (10)
// Normal constructor (11)
// Copy constructor (12)
// 이동 의미론 적용 x
void swapCopy(T& a, T& b){
T temp(a);
a = b;
b = temp;
}
// 이동 의미론 적용 o
void swapMove(T& a, T& b){
T temp(std::move(a));
a = std::move(b);
b = std::move(temp);
}
class SpreadsheetCell
{
// 생략
private:
static std::string doubleToString(double inValue);
static double stringToDouble(std::string_view inString);
// 생략
};
// stringToDouble()과 doubleToString()을 public으로 선언하면
// 클래스 외부에서 호출 가능
string str = SpreadsheetCell::doubleToString(5.0);
class Spreadsheet
{
public:
// 생략
double getValue() const;
std::string getString() const;
// 생략
};
SpreadsheetCell myCell(5);
cout << myCell.getValue() << endl; // ok
myCell.setString("6"); // ok
const SpreadsheetCell& myCellConstRef = myCell;
cout << myCellConstRef.getValue() << endl; // ok
myCellConstRef.setString("6"); // 컴파일 에러
class Spreadsheet
{
// 생략
private:
double mValue = 0;
mutable size_t mNumAccesses = 0;
};
// getValue() & getString() 정의
double SpreadsheetCell::getValue() const
{
mNumAccesses++;
return mValue;
}
std::string SpreadsheetCell::getString() const
{
mNumAccesses++;
return doubleToString(mValue);
}
class Spreadsheet
{
public:
// 생략
void set(double inValue);
void set(std::string_view inString);
// 생략
};
class Spreadsheet
{
public:
SpreadsheetCell& getCellAt(size_t x, size_t y);
const SpreadsheetCell& getCellAt(size_t x, size_t y) const;
// 생략
};
class MyClass
{
public:
void foo(int i);
void foo(double i) = delete;
};
// 호출
MyClass c;
c.foo(123);
c.foo(1.23); // 컴파일 에러 발생!
inline double SpreadsheetCell::getValue() const{
mNumAccesses++;
return mValue;
}
인라인 베서드는 반드시 프로토타입과 구현 코드를 헤더 파일에 작성해야 함!
class Spreadsheet
{
public:
Spreadsheet(size_t width = 100, size_t height = 100);
// 생략
};
// 호출
Spreadsheet s1;
Spreadsheet s2(5);
Spreadsheet s3(5, 6);
class Spreadsheet
{
// 생략
private:
static size_t sCounter; // 기본적으로 0 으로 초기화 되어짐 static 포인터는 nullptr로 초기화
};
class Spreadsheet
{
// 생략
private:
static inline size_t sCounter = 0;
};
// 위와 같이 작성하면 소스 파일에 아래 부분을 적지 않아도 된다.
size_t Spreadsheet::sCounter;
// mId라는 데이터 멤버를 만듦
// mId를 Spreadsheet 생성자에서 SCounter의 값으로 초기화하는 경우
class Spreadsheet
{
public:
// 생략
size_t getId() const;
private:
// 생략
static size_t sCounter;
size_t mId = 0;
};
Spreadsheet::Spreadsheet(size_t width, size_t height)
: mId(sCounter++), mWidth(width), mHeight(height)
{
mCells = new SpreadsheetCell*[mWidth];
for (size_t i = 0; i < mWidth; i++) {
mCells[i] = new SpreadsheetCell[mHeight];
}
}
class Spreadsheet
{
public:
// 생략
static const size_t kMaxHeight = 100;
static const size_t kMaxWidth = 100;
}
Spreadsheet::Spreadsheet(size_t width, size_t height,
const SpreadsheetApplication& theApp)
: mId(sCounter++)
, mWidth(std::min(width, kMaxWidth)) // min()은 <algorithm> 에 있음!
, mHeight(std::min(height, kMaxHeight))
{
mCells = new SpreadsheetCell*[mWidth];
for (size_t i = 0; i < mWidth; i++) {
mCells[i] = new SpreadsheetCell[mHeight];
}
}
class SpreadsheetApplication; // forward declaration
class Spreadsheet
{
public:
Spreadsheet(size_t width, size_t height,
SpreadsheetApplication& theApp);
// 생략
private:
// 생략
SpreadsheetApplication& mTheApp;
};
Spreadsheet::Spreadsheet(size_t width, size_t height,
const SpreadsheetApplication& theApp) // 추가된 부분
: mId(sCounter++)
, mWidth(std::min(width, kMaxWidth))
, mHeight(std::min(height, kMaxHeight))
, mTheApp(theApp) // 추가된 부분
{
// 생략
}
class Spreadsheet
{
public:
Spreadsheet(size_t width, size_t height,
const SpreadsheetApplication& theApp); // 변경 부분
// 생략
private:
// 생략
const SpreadsheetApplication& mTheApp; // 변경 부분
};
class Spreadsheet
{
public:
class Cell
{
public:
Cell() = default;
Cell(double initialValue);
// 생략
};
Spreadsheet(size_t width, size_t height,
const SpreadsheetApplication& theApp);
// 생략
}
class SpreadsheetCell
{
public:
// 생략
enum class Color { Red = 1, Green, Blue, Yellow };
void setColor(Color color);
Color getColor() const;
private:
// 생략
Color mColor = Color::Red;
};
// 메서드 구현 정의
double getValue() const { mNumAccesses++; return mValue; }
std::string getString() const { mNumAccesses++; return doubleToString(mValue); }
// 메서드 사용
SpreadsheetCell myCell(5);
myCell.setColor(SpreadsheetCell::Color::Blue);
auto color = myCell.getColor();
class SpreadsheetCell
{
public:
// 생략
// 원본 셀을 변경하지 않도록 const로 선언
SpreadsheetCell add(const SpreadsheetCell& cell) const;
// 생략
};
SpreadsheetCell SpreadsheetCell::add(const SpreadsheetCell& cell) const
{
return SpreadsheetCell(getValue() + cell.getValue());
}
SpreadsheetCell myCell(4), anotherCell(5);
SpreadsheetCell aThirdCell = myCell.add(anotherCell);
class SpreadsheetCell{
public:
// 생략
SpreadsheetCell operator+(const SpreadsheetCell& cell) const;
// 생략
};
SpreadsheetCell SpreadsheetCell::operator+(const SpreadsheetCell& cell) const
{
return SpreadsheetCell(getValue() + cell.getValue());
}
묵시적 변환
- operator+를 정의하면 셀끼리 더할 수 있을 뿐 아니라, 셀에 string_view, duoble, int와 같은 값도 더할 수 있음.
- 이는 컴파일러가 operator+만 찾는 데 그치지 않고 타입을 변활할 수 있는 방법도 찾기 때문.
- 묵시적으로 변환하지 않게 하려면 explicite 키워드를 붙임.
class SpreadsheetCell{
public:
// 생략
explicit SpreadsheetCell(std::string_view initialValue);
// 생략
};
class SpreadsheetCell
{
// 생략
}
// 전역 함수로 정의하려면 연산자를 헤더파일에 선언
SpreadsheetCell operator+(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs);
aThirdCell = myCell + 5.6;
aThirdCell = myCell + 4;
aThirdCell = 4 + myCell; // 문제 없이 실행된다.
aThirdCell = 5.6 + myCell; // 문제 없이 실행된다.
aThirdCell = 4.5 + 5.5; // 컴파일 에러는 없지만, 정의한 operator+를 호출하지 않음
// 축약형 산술 연산자의 선언
class SpreadsheetCell
{
public:
// 생략
SpreadsheetCell& operator+=(const SpreadsheetCell& rhs);
SpreadsheetCell& operator-=(const SpreadsheetCell& rhs);
SpreadsheetCell& operator*=(const SpreadsheetCell& rhs);
SpreadsheetCell& operator/=(const SpreadsheetCell& rhs);
// 생략
};
// operator+=의 구현 (다른 연산자도 비슷)
SpreadsheetCell& SpreadsheetCell::operator+=(const SpreadsheetCell& rhs)
{
set(getValue() + rhs.getValue());
return *this;
}
// operator+=의 사용
SpreadsheetCell myCell(4), aThirdCell(2);
aThirdCell -= myCell;
aThirdCell += 5.4;
5.4 += aThirdCell; // 작성 불가!
<op>
자리에 비교 연산자 여섯 개가 적용되도록 선언.class SpreadsheetCell
{
// 생략
};
bool operator==(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs);
bool operator<(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs);
bool operator>(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs);
bool operator!=(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs);
bool operator<=(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs);
bool operator>=(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs);
bool operator==(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs)
{
return (lhs.getValue() == rhs.getValue());
}
연산자 오버로딩
핵심은 클래스를 최대한 int or double과 같은 기본 타입에 가깝게 정의!
이를 핌플 이디엄(브릿지 패턴) 이라고 한다.
// Spreadsheet 클래스에 브릿지 패턴 적용
#include "SpreadsheetCell.h"
#include <memory>
// 전방 선언
class SpreadsheetApplication;
// public interface class로 정의
class Spreadsheet
{
public:
Spreadsheet(const SpreadsheetApplication& theApp,
size_t width = kMaxWidth, size_t height = kMaxHeight);
Spreadsheet(const Spreadsheet& src);
~Spreadsheet();
Spreadsheet& operator=(const Spreadsheet& rhs);
void setCellAt(size_t x, size_t y, const SpreadsheetCell& cell);
SpreadsheetCell& getCellAt(size_t x, size_t y);
size_t getId() const;
static const size_t kMaxHeight = 100;
static const size_t kMaxWidth = 100;
friend void swap(Spreadsheet& first, Spreadsheet& second) noexcept;
private:
// 구현코드는 Impl이라는 이름의 private 중첩 클래스로 정의
// => Spreadsheet class는 Impl 인스턴스에 대한 표인터 데이터 멤버 하나만 갖게 됨.
class Impl;
std::unique_ptr<Impl> mImpl;
};
#include <cstddef>
#include "Spreadsheet.h"
#include "SpreadsheetCell.h"
class Spreadsheet::Impl
{
public:
Impl(const SpreadsheetApplication& theApp,
size_t width, size_t height);
Impl(const Impl& src);
~Impl();
Impl& operator=(const Impl& rhs);
void setCellAt(size_t x, size_t y, const SpreadsheetCell& cell);
SpreadsheetCell& getCellAt(size_t x, size_t y);
size_t getId() const;
private:
void verifyCoordinate(size_t x, size_t y) const;
// Impl class는 Spreadsheet의 중첩 클래스이므로 Impl 객체를 맞바꾸는 전역 friend swap() 함수를 private 메서드로 정의
void swap(Impl& other) noexcept;
size_t mId = 0;
size_t mWidth = 0;
size_t mHeight = 0;
SpreadsheetCell** mCells = nullptr;
const SpreadsheetApplication& mTheApp;
static size_t sCounter;
};
// 중첩 클래스 이므로, Spreadsheep::Impl::swap() 으로 지정해야 함. 다른 멤버도 마찬가지
void Spreadsheet::Impl::swap(Impl& other) noexcept
{
using std::swap;
swap(mWidth, other.mWidth);
swap(mHeight, other.mHeight);
swap(mCells, other.mCells);
}
Spreadsheet::Impl& Spreadsheet::Impl::operator=(const Impl& rhs)
{
// 자신을 대입하는지 확인한다.
if (this == &rhs) {
return *this;
}
// 복제 후 맞바꾸기(copy-and-swap) 패턴 적용
Impl temp(rhs); // 모든 작업을 임시 인스턴스에서 처리한다.
swap(temp); // 예외를 발생하지 않는 연산으로만 처리한다.
return *this;
}
추상 인터페이스 즉, 가상 메서드로만 구성된 인터페이스를 정의한 뒤 이를 구현하는 클래스를 따로 작성해도 가능.
내가 생각했을땐, 브릿지 패턴보다는 추상 인터페이스와 가상 메서드로 분리하는 방법이 더 간단해 보인다..
다음 장에서는 상속에 대한 내용을 알아보자.