이동 생성자(move constructor)가 호출되지 않습니다.

안녕하세요. 가입한지 6개월 넘게 눈팅…만 하고 있다가 첫 질문을 올리게 된 코린이 입니다.
우분투 g++ 환경에서 dynamic array를 구현중인데, 기본 생성자와 복사 생성자만 호출되고 이동 생성자가 호출 되지 않는 상황입니다.

저는 main.cpp의 IVector v3(v1+v2); 부분에서 v1+v2가 rvalue라 이동생성자가 호출되기를 기대했는데, 코드를 어떻게 바꿔 짜도 계속해서 기본 생성자나 복사 생성자만 호출됩니다. 구글링으로 이동 생성자와 +연산자 오버로딩 함수의 예시를 찾아보았지만, 제가 놓치는 부분이 있는지 생각대로 구현되지 않았고요. 코드를 어떻게 바꿔 짜야 이동생성자가 호출 될 수 있을까요?

아래는 코드인데, 혹시 몰라 전체를 올려봅니다.

IVector.h

#ifndef IVECTOR
#define IVECTOR

class IVector{
private:
	int *ptr=nullptr;
	int m_size=0;
	int m_capacity=0;
	const int initial_size=5;
public:
	IVector();
	IVector(IVector &v);
	IVector(IVector &&v);
	~IVector();

	long Capacity();
	long Size();
	void push_back(int value);
	int pop_back();

	IVector& operator=(const IVector &v);
	int& operator[](const int index);
	IVector operator+(const IVector &v);
};

#endif

IVector.cpp

#include <iostream>
#include "ivector.h"

IVector::IVector(){
	//printf("default constructor\n");
	m_size=0;
	m_capacity=initial_size;
	ptr=new int[initial_size];
}

IVector::IVector(IVector &v){
	//printf("copy constructor\n");
	m_size=static_cast<int>(v.Size());
	m_capacity=static_cast<int>(v.Capacity());
	ptr=new int[m_capacity];
	for(int i=0; i<m_size; ++i){
		ptr[i]=v[i];
	}
}

IVector::IVector(IVector &&v){
	//printf("move constructor\n");
	m_size=v.m_size;
	m_capacity=v.m_capacity;
	ptr=v.ptr;

	v.ptr=nullptr;
}

IVector::~IVector(){
	//printf("destructor\n");
	if(ptr){
		delete[] ptr;
	}
	ptr=nullptr;
}

long IVector::Capacity(){
	return static_cast<long>(m_capacity);
}

long IVector::Size(){
	return static_cast<long>(m_size);
}

void IVector::push_back(int value){
	if(m_size==m_capacity){
		int *tmp=new int[m_capacity+initial_size];

		for(int i=0; i<m_size; ++i){
			tmp[i]=ptr[i];
		}
		
		delete[] ptr;
		m_capacity+=initial_size;
		ptr=tmp;
	}

	ptr[m_size++]=value;
	//printf("push_back %d\n", ptr[m_size-1]);
}

int IVector::pop_back(){
	return ptr[--m_size];
}

IVector& IVector::operator=(const IVector &v){
	//printf("=opertor\n");
	if(this!=&v){
		m_size=static_cast<int>(v.m_size);
		m_capacity=static_cast<int>(v.m_capacity);
		
		if(ptr){
			delete []ptr;
		}
		ptr=new int[m_capacity];
	
		for(int i=0; i<m_size; ++i){
			ptr[i]=v.ptr[i];
		}
	}

	return *this;
}

int& IVector::operator[](const int index){
	if(index>=m_size || index<0){
		std::cout<<"out of range, unknown value returns"<<std::endl;
	}
	return ptr[index];
}

IVector IVector::operator+(const IVector &v){
	//printf("+ operator\n");
	IVector tmpvector;
	int i;

	tmpvector.m_size=m_size+v.m_size;
	tmpvector.m_capacity=m_capacity+v.m_capacity;
	tmpvector.ptr=new int[tmpvector.m_capacity];

	for(i=0; i<m_size; ++i){
		tmpvector.ptr[i]=ptr[i];
	}
	for(; i<m_size+v.m_size; ++i){
		tmpvector.ptr[i]=v.ptr[i-m_size];
	}

	return tmpvector;
}

main.cpp

#include <iostream>
#include "ivector.h"

int main(void){
	printf("IVector v1\n");
	IVector v1;
	printf("v1.push_back(1)\n");
	v1.push_back(1);
	printf("v1.push_back(2)\n");
	v1.push_back(2);
	printf("IVector v2(v1)\n");
	IVector v2(v1);
	printf("IVector v3(v1+v2)\n");
	IVector v3(v1+v2); //move constructor (x)
	printf("IVector v4\n");
	IVector v4;
	printf("v4=v3\n");
	v4=v3;
	printf("v4=v1+v3\n");
	v4=v1+v3;

	printf("v1[0]=3\n");
	v1[0]=3;

	std::cout<<v1[1]<<'\n';
	std::cout<<v1.pop_back()<<std::endl;

	return 0;
}
2 Likes

안녕하세요.

제가 비주얼스튜디오로 했을때는 잘 실행되더라고요.

1 Like

이동생성자와 이동대입연산자에 noexcept를 붙여보세요

1 Like

으음… 역시 g++ 컴파일러가 문제인 걸까요…
확실히 g++ 환경에서 move 생성자가 어쩌고 하는 글들이 많았는데, 역시 컴파일러 문제였나 싶기도 하고…
일단 VS에서는 잘 된다는 걸 알았으니 컴파일러 쪽으로 알아보겠습니다!

noexcept 얘기도 구글링에서 많이들 나와서 적용해봤었는데, 아쉽게도 결과가 달라지진 않았습니다.
그렇다면 이동생성자, 이동대입연산자 함수 내부의 구현 문제일까요…?

g++해봤는데 정말안대네요… 머지… (ㅇㅅㅇ);; 당황

1 Like

하… 이 삽질이 제 잘못이 아니었다니… 감동의 눈물이 다 나네요 진짜 ㅋㅋ큐ㅠㅠㅠㅠ

네 구현이 이상해보이네요. operator+A + B 형태로 불려야 하니 non-member function이어야 할 것 같은데요. 그리고 move constructor를 만들어 줬으면 move assignment operator도 만들어줘야 합니다

2 Likes

일단 non-member function이어야 할 것 같아 operator+를 friend로 구현해보았고요, move assignment operator의 존재를 처음으로 알게 되어 이것 역시 구현해봤습니다.
덕분에 move assignment operator가 copy assignment operator와 구분되어 사용되는 걸 확인했습니다!
그래도 이동 생성자가 호출되는 결과가 나오진 않더라고요.
바쁘지 않으시다면 다시 코드를 봐주실 수 있을까요…?

ivector.h

#ifndef IVECTOR
#define IVECTOR

class IVector{
private:
	int *ptr=nullptr;
	int m_size=0;
	int m_capacity=0;
	const int initial_size=5;
public:
	IVector();
	IVector(IVector &v);
	IVector(IVector &&v)noexcept;
	~IVector();

	long Capacity();
	long Size();
	void push_back(int value);
	int pop_back();

	IVector& operator=(const IVector &v)noexcept;
	IVector& operator=(const IVector &&v)noexcept;
	int& operator[](const int index);
	IVector operator+(const IVector &v);
	friend IVector operator+(const IVector &v1, const IVector &v2)
		{return v1+v2;}
};

#endif

ivector.cpp

#include <iostream>
#include "ivector.h"

IVector::IVector(){
	printf("default constructor\n");
	m_size=0;
	m_capacity=initial_size;
	ptr=new int[initial_size];
}

IVector::IVector(IVector &v){
	printf("copy constructor\n");
	m_size=static_cast<int>(v.Size());
	m_capacity=static_cast<int>(v.Capacity());
	ptr=new int[m_capacity];
	for(int i=0; i<m_size; ++i){
		ptr[i]=v[i];
	}
}

IVector::IVector(IVector &&v)noexcept{
	printf("move constructor\n");
	m_size=v.m_size;
	m_capacity=v.m_capacity;
	ptr=v.ptr;

	v.ptr=nullptr;
}

IVector::~IVector(){
	printf("destructor\n");
	if(ptr){
		delete[] ptr;
	}
	ptr=nullptr;
}

long IVector::Capacity(){
	return static_cast<long>(m_capacity);
}

long IVector::Size(){
	return static_cast<long>(m_size);
}

void IVector::push_back(int value){
	if(m_size==m_capacity){
		int *tmp=new int[m_capacity+initial_size];

		for(int i=0; i<m_size; ++i){
			tmp[i]=ptr[i];
		}
		
		delete[] ptr;
		m_capacity+=initial_size;
		ptr=tmp;
	}

	ptr[m_size++]=value;
	//printf("push_back %d\n", ptr[m_size-1]);
}

int IVector::pop_back(){
	return ptr[--m_size];
}

IVector& IVector::operator=(const IVector &v)noexcept{
	printf("= copy operator\n");
	if(this!=&v){
		m_size=static_cast<int>(v.m_size);
		m_capacity=static_cast<int>(v.m_capacity);
		
		if(ptr){
			delete []ptr;
		}
		ptr=new int[m_capacity];
	
		for(int i=0; i<m_size; ++i){
			ptr[i]=v.ptr[i];
		}
	}

	return *this;
}

IVector& IVector::operator=(const IVector &&v)noexcept{
	printf("= move operator\n");
	if(this!=&v){
		m_size=static_cast<int>(v.m_size);
		m_capacity=static_cast<int>(v.m_capacity);

		if(ptr){
			delete []ptr;
		}
		ptr=new int[m_capacity];

		for(int i=0; i<m_size; ++i){
			ptr[i]=v.ptr[i];
		}
	}

	return *this;
}

int& IVector::operator[](const int index){
	if(index>=m_size || index<0){
		std::cout<<"out of range, unknown value returns"<<std::endl;
	}
	return ptr[index];
}

IVector IVector::operator+(const IVector &v){
	printf("+ operator\n");
	IVector tmpvector;
	int i;

	tmpvector.m_size=m_size+v.m_size;
	tmpvector.m_capacity=m_capacity+v.m_capacity;
	tmpvector.ptr=new int[tmpvector.m_capacity];

	for(i=0; i<m_size; ++i){
		tmpvector.ptr[i]=ptr[i];
	}
	for(; i<m_size+v.m_size; ++i){
		tmpvector.ptr[i]=v.ptr[i-m_size];
	}

	return tmpvector;
}

코드

#include <iostream>

class test {
public:

    test() {
        
    }
    
    test(test& t) noexcept {
        std::cout << "copy c" << std::endl;
    }
    
    test(test&& t) noexcept {
        std::cout << "move c" << std::endl;
    }
    
    test& operator=(const test &t) {
        std::cout << "copy a" << std::endl;
    }
    
    test& operator=(test &&t) {
        std::cout << "move a" << std::endl;
    }
};

int main()
{
    test a;
    test b(a);
    test c(std::move(b));
    test d = a;
    test e = std::move(d);

    return 0;
}

실행결과

copy c                                                                                                                                                                                                                                                                                                                                                          
move c                                                                                                                                                                                                                                                                                                                                                          
copy c                                                                                                                                                                                                                                                                                                                                                          
move c

그냥 테스트로 해봤는데여, 흠… 올리신 코드에 뭐가 문제일까요… 끄응…

1 Like

move assignment 연산자는 const 매개변수면 안대여 ^^/

IVector& IVector::operator=(const IVector &&v)noexcept{
	printf("= move operator\n");
	if(this!=&v){
		m_size=static_cast<int>(v.m_size);
		m_capacity=static_cast<int>(v.m_capacity);

		if(ptr){
			delete []ptr;
		}
		ptr=new int[m_capacity];

		for(int i=0; i<m_size; ++i){
			ptr[i]=v.ptr[i];
		}
	}

	return *this;
}

이동했으면 기존에 있는 애는 ptr을 nullptr로 지워줘야죠 ㅎㅎ

1 Like

std::move 를 사용하면 move constructor가 호출되긴 합니다… 근데 구현에서 이걸 요구하는 게 아닌 것 같아서 크흠…ㅠㅠ

아 맞다 감사합니다

operator+가 local object를 리턴하고 있네요. 이걸 생성자 안에서 v3(v1 + v2) 식으로 부르면 컴파일러가 성능 최적화를 위해 copy elision을 일으켜서 별도의 생성자를 부르지 않고 즉시 v1 + v2가 만든 object를 v3 자리에 끼워넣어 생성합니다. move constructor가 불리지 않는 현상은 자연스러운 현상이며, std::move를 붙이는 것은 오히려 컴파일러의 최적화를 방해하는 행동입니다. 사실 가장 좋은 방법은 소멸자 / 복사생성자 / 복사대입연산자 / 이동생성자 / 이동대입연산자를 직접 만들지 않고 컴파일러에게 맡기는 것이에요.

이동생성자가 호출되는 것을 굳이 보고 싶다면 이런 식으로 형변환을 일으켜보시면 됩니다.

#include <iostream>

struct B
{};

struct A
{
  A() {}
  A(A&& a) {
    std::cout << "move" << std::endl;
  }
  A(B&& b) {
    std::cout << "move from B" << std::endl;
  }
};


int main()
{
  A a1 = A(); // move elided
  A a2 = B(); // move not elided because of type conversion
  return 0;
}
2 Likes

아 저도 들어본적 있어요.

정말 그렇겠네요. ^^/ 우왕~ 갓갓 최적화

세상에나… 최적화가 여기까지 손길을 뻗치는 군요