C ++ 참조 변수를 반환하는 관행이 사악합니까?


341

이것은 내가 생각하는 약간 주관적입니다. 의견이 만장일치인지 확실하지 않습니다 (참조가 반환되는 많은 코드 스 니펫을 보았습니다).

이 질문 에 대한 의견에 따르면 , 참조 초기화와 관련하여 방금 요청한 참조를 삭제하는 것이 더 쉽게 누락되어 메모리 누수가 발생할 수 있으므로 참조 를 반환하는 것은 악의적 일 수 있습니다.

나는 내가 예제를 따르고 (내가 상상하지 않는 한) 공정한 몇 곳에서 이것을 했으므로 걱정이됩니다 ... 오해 했습니까? 사악한가요? 그렇다면 얼마나 악한가?

필자는 C ++을 처음 접했다는 사실과 결합 된 포인터와 참조가 혼합되어 있기 때문에 언제 사용해야 할 지에 대한 혼란이 내 응용 프로그램에서 메모리 누수 문제라고 생각합니다.

또한 스마트 / 공유 포인터 사용은 일반적으로 메모리 누수를 피하는 가장 좋은 방법으로 받아 들여진다는 것을 알고 있습니다.


게터와 같은 함수 / 메서드를 작성하는 것은 악한 일이 아닙니다.
John Z. Li

답변:


411

일반적으로 참조 반환은 완벽하게 정상이며 항상 발생합니다.

당신이 의미하는 경우 :

int& getInt() {
    int i;
    return i;  // DON'T DO THIS.
}

그것은 모든 종류의 악입니다. 스택 할당 i이 사라지고 아무것도 언급하지 않습니다. 이것은 또한 악하다 :

int& getInt() {
    int* i = new int;
    return *i;  // DON'T DO THIS.
}

이제 클라이언트는 결국 이상한 일을해야하기 때문에 :

int& myInt = getInt(); // note the &, we cannot lose this reference!
delete &myInt;         // must delete...totally weird and  evil

int oops = getInt(); 
delete &oops; // undefined behavior, we're wrongly deleting a copy, not the original

rvalue 참조는 여전히 참조 일 뿐이므로 모든 악의적 인 응용 프로그램은 동일하게 유지됩니다.

함수의 범위를 벗어나는 것을 할당하려면 스마트 포인터 (또는 일반적으로 컨테이너)를 사용하십시오.

std::unique_ptr<int> getInt() {
    return std::make_unique<int>(0);
}

이제 클라이언트는 스마트 포인터를 저장합니다.

std::unique_ptr<int> x = getInt();

다음과 같이 수명이 더 높은 수준으로 공개되어있는 것을 액세스하는 경우에도 참조가 가능합니다.

struct immutableint {
    immutableint(int i) : i_(i) {}

    const int& get() const { return i_; }
private:
    int i_;
};

여기서 우리 i_가 호출하는 것이 클래스 인스턴스의 수명을 관리하므로 i_적어도 그 수명을 유지 하기 때문에 참조를 반환해도 괜찮습니다 .

그리고 물론, 단지 아무 문제가 없습니다 :

int getInt() {
   return 0;
}

평생을 발신자에게 맡겨야하고 가치를 계산하는 것입니다.

요약 : 호출 후 객체의 수명이 끝나지 않으면 참조를 반환해도됩니다.


21
이것들은 모두 나쁜 예입니다. 올바른 사용법의 가장 좋은 예는 전달 된 객체에 대한 참조를 반환 할 때입니다. ala operator <<
Arelius

171
후손을 위해, 그리고 이것에 찬성하는 새로운 프로그래머 에게는 포인터가 나쁘지 않습니다 . 동적 메모리에 대한 포인터도 나쁘지 않습니다. 둘 다 C ++에서 합법적 인 장소를 가지고 있습니다. 스마트 포인터는 동적 메모리 관리와 관련하여 기본적으로 사용되어야하지만 기본 스마트 포인터는 shared_ptr이 아니라 unique_ptr이어야합니다.
Jamin Gray

12
승인자 수정 : 수정 사항이 정확한지 보증 할 수없는 경우 수정 사항을 승인하지 않습니다. 잘못된 편집을 롤백했습니다.
GManNickG

7
후손을 위해, 그리고 이것에 찬성하는 새로운 프로그래머를 위해 글을 쓰지 마십시오return new int .
궤도에서 가벼움 레이스

3
후손을 위해, 그리고 이것을 선호하는 새로운 프로그래머를 위해, 함수에서 T를 반환하십시오. RVO는 모든 것을 처리합니다.
구두

64

아냐 아냐 아냐 아냐

악의적 인 것은 동적으로 할당 된 객체를 참조하고 원래 포인터를 잃는 것입니다. 당신 new이 물건을 할 때 당신 은 보장을받을 의무가 있다고 가정합니다 delete.

그러나 예를 들어 operator<<: 참조 반환 해야하는 것을 보거나

cout << "foo" << "bar" << "bletch" << endl ;

작동하지 않습니다.


23
나는 이것이 OP (삭제가 필요하다는 것을 분명히 한 OP)가 질문에 대답하지도 않았고 freestore 객체에 대한 참조를 반환하는 것이 혼란을 초래할 수 있다는 합법적 인 두려움을 다루지 않기 때문에 하향 투표했다. 한숨.

4
참조 객체를 반환하는 관행은 나쁘지 않습니다 . 에르고 두 번째 그래프에서 지적했듯이 그가 표현하는 두려움은 올바른 두려움입니다.
Charlie Martin

당신은 실제로하지 않았다. 그러나 이것은 내 시간의 가치가 없습니다.

2
Iraimbilanja @ "아니오"에 대해 걱정하지 않습니다. 그러나이 게시물은 GMan에서 누락 된 중요한 정보를 지적했습니다.
Kobor42

48

즉시 사라지지 않고 소유권 이전을 원하지 않는 기존 객체에 대한 참조를 반환해야합니다.

로컬 변수 또는 그와 같은 일부에 대한 참조는 반환하지 않으므로 절대로 반환하지 마십시오.

함수와 무관 한 것에 대한 참조를 리턴 할 수 있으며, 호출 함수가 삭제를 담당하지 않을 것으로 예상됩니다. 일반적인 operator[]기능 의 경우입니다 .

무언가를 생성하는 경우 값 또는 포인터 (일반 또는 스마트)를 반환해야합니다. 호출 함수에서 변수 또는 표현식으로 들어가므로 값을 자유롭게 리턴 할 수 있습니다. 로컬 변수에 대한 포인터는 반환되지 않으므로 절대 반환하지 마십시오.


1
훌륭한 답변이지만 "임시 참조를 const 참조로 반환 할 수 있습니다." 다음 코드는 return 문 끝에서 임시가 파괴되므로 컴파일되지만 충돌이 발생합니다. "int const & f () {return 42;} void main () {int const & r = f (); ++ r;} "
j_random_hacker

@j_random_hacker : C ++에는 임시 수명이 연장 될 수있는 임시 참조에 대한 이상한 규칙이 있습니다. 사건에 해당되는지 알 수 없어서 죄송합니다.
Mark Ransom

3
@ 마크 : 예, 이상한 규칙이 있습니다. 임시 수명은 const 참조 (클래스 멤버가 아님)를 초기화해야만 연장 할 수 있습니다. 그런 다음 심판이 범위를 벗어날 때까지 생존합니다. 안타깝게도 const 참조를 반환하는 것은 다루지 않습니다 . 그러나 값으로 온도를 반환하는 것이 안전합니다.
j_random_hacker

C ++ 표준, 12.2, 단락 5를 참조하십시오 . herbsutter.wordpress.com/2008/01/01/… 에서 Herb Sutter의 금주의 길을 보라 .
David Thornley

4
@David : 함수의 반환 유형이 "T const &"인 경우 실제로 발생하는 것은 return 문 이 6.6.3.2에 따라 T 유형의 temp를 "T const &"유형으로 암시 적으로 변환 하는 것입니다 (법적 변환이지만 1 호출 코드는 함수의 결과로 "T const &"유형의 참조를 초기화하고, "T const &"유형도 다시 합법적이지만 수명 연장이 아닌 프로세스로 초기화합니다. 최종 결과 : 수명 연장이없고 혼동이 심합니다. :(
j_random_hacker

26

답변이 만족스럽지 않아서 2 센트를 더하겠습니다.

다음과 같은 경우를 분석해 봅시다.

잘못된 사용법

int& getInt()
{
    int x = 4;
    return x;
}

이것은 분명히 오류입니다

int& x = getInt(); // will refer to garbage

정적 변수와 함께 사용

int& getInt()
{
   static int x = 4;
   return x;
}

정적 변수는 프로그램 수명 기간 동안 존재하기 때문에 이것이 맞습니다.

int& x = getInt(); // valid reference, x = 4

싱글 톤 패턴을 구현할 때도 매우 일반적입니다.

Class Singleton
{
    public:
        static Singleton& instance()
        {
            static Singleton instance;
            return instance;
        };

        void printHello()
        {
             printf("Hello");
        };

}

용법:

 Singleton& my_sing = Singleton::instance(); // Valid Singleton instance
 my_sing.printHello();  // "Hello"

연산자

표준 라이브러리 컨테이너는 참조를 반환하는 연산자의 사용에 크게 의존합니다 (예 :

T & operator*();

다음에서 사용될 수 있습니다

std::vector<int> x = {1, 2, 3}; // create vector with 3 elements
std::vector<int>::iterator iter = x.begin(); // iterator points to first element (1)
*iter = 2; // modify first element, x = {2, 2, 3} now

내부 데이터에 빠르게 액세스

내부 데이터에 빠르게 액세스하기 위해 &를 ​​사용하는 경우가 있습니다

Class Container
{
    private:
        std::vector<int> m_data;

    public:
        std::vector<int>& data()
        {
             return m_data;
        }
}

사용법 :

Container cont;
cont.data().push_back(1); // appends element to std::vector<int>
cont.data()[0] // 1

그러나 이것은 다음과 같은 함정으로 이어질 수 있습니다.

Container* cont = new Container;
std::vector<int>& cont_data = cont->data();
cont_data.push_back(1);
delete cont; // This is bad, because we still have a dangling reference to its internal data!
cont_data[0]; // dangling reference!

정적 변수에 대한 참조를 반환하면 원하지 않는 동작이 발생할 수 있습니다. 예를 들어 정적 멤버에 대한 참조를 반환하는 곱하기 연산자를 고려하면 다음과 같은 결과가 항상 나타납니다 true.If((a*b) == (c*d))
SebNag

Container::data()구현은 다음과 같아야합니다return m_data;
Xeaz

감사합니다! @Xeaz는 추가 호출에 문제를 일으키지 않습니까?
앤드류

@SebTu 왜 그렇게 하시겠습니까?
Thorhunter

@Andrew 아니오, 그것은 구문 shenanigan이었습니다. 예를 들어 포인터 유형을 반환 한 경우 참조 주소를 사용하여 포인터를 만들고 반환합니다.
Thorhunter

14

악하지 않습니다. C ++의 많은 것들과 마찬가지로 올바르게 사용하면 좋지만 지역 변수에 대한 참조를 반환하는 것과 같이 사용할 때 알아야 할 많은 함정이 있습니다.

그것으로 달성 할 수있는 좋은 것들이 있습니다 (예 : map [name] = "hello world")


1
궁금해서 궁금한 점은 map[name] = "hello world"무엇입니까?
wrongusername

5
@wrongusername 구문은 직관적입니다. HashMap<String,Integer>Java에 저장된 값의 수를 늘리려 고 시도한 적이 있습니까? : P
Mehrdad Afshari

Haha, 아직은 아니지만 HashMap 예제를 살펴보면 꽤
나쁘게 보입니다

내가 가진 문제 : 함수는 컨테이너의 객체에 대한 참조를 반환하지만 호출 함수 코드는이를 로컬 변수에 할당했습니다. 그런 다음 객체의 일부 속성을 수정했습니다. 문제 : 컨테이너의 원본 개체는 그대로 유지됩니다. 프로그래머는 리턴 값에서 &를 쉽게 간과하고 예상치 못한 동작을 얻을 수 있습니다.
flohack

10

"참조를 반환하는 것은 악의적이다. 왜냐하면 단순히 [이해 한 것처럼] 그것을 삭제하기가 더 쉽기 때문이다"

사실이 아니다. 참조를 반환한다고해서 소유권 의미가있는 것은 아닙니다. 즉, 당신이 이것을하기 때문에 :

Value& v = thing->getTheValue();

... v가 참조하는 메모리를 소유한다는 의미는 아닙니다.

그러나 이것은 끔찍한 코드입니다.

int& getTheValue()
{
   return *new int;
}

"해당 인스턴스에 포인터가 필요하지 않기 때문에 " 이와 같은 작업을 수행하는 경우 1) 참조가 필요한 경우 포인터를 역 참조하고 2) 결국 포인터가 필요합니다. 새로운 삭제, 당신은 삭제 호출 포인터가 필요합니다.


7

두 가지 경우가 있습니다.

  • const reference-때로는 무거운 객체 또는 프록시 클래스, 컴파일러 최적화에 대한 좋은 아이디어

  • 비 const 참조-때로는 나쁜 생각으로 캡슐화가 깨짐

둘 다 동일한 문제를 공유합니다-잠재적으로 파괴 된 객체를 가리킬 수 있습니다 ...

참조 / 포인터를 반환 해야하는 많은 상황에서 스마트 포인터를 사용하는 것이 좋습니다.

또한 다음 사항에 유의하십시오.

C ++ 표준 (관심있는 경우 섹션 13.3.3.1.4)에 임시 규칙은 const 참조에만 바인딩 할 수 있다고 공식적인 규칙이 있습니다-비 const 참조를 사용하려고하면 컴파일러는이 플래그를 오류.


1
비 const 참조는 반드시 캡슐화를 깨뜨릴 필요는 없습니다. 고려 vector :: operator []

그것은 매우 특별한 경우입니다 ... 그렇기 때문에 나는 때때로 가장 많은 시간을 청구해야하지만 :)

그렇다면 일반적인 첨자 연산자 구현에 필요한 악이 있다고 말하고 있습니까? 나는 이것에 동의하지 않거나 동의하지 않습니다. 나는 더 현명한 사람이 아닙니다.
Nick Bolton

나는 말하지 않지만, 잘못 사용하면 악할 수 있습니다 :)))) vector :: at는 가능할 때마다 사용해야합니다 ....

뭐라고? vector :: at은 또한 비 const 참조를 반환합니다.

4

그것은 악하지 않을뿐만 아니라 때로는 필수적입니다. 예를 들어 참조 반환 값을 사용하지 않고 std :: vector의 [] 연산자를 구현하는 것은 불가능합니다.


아, 물론입니다. 이것이 내가 사용하기 시작한 이유라고 생각합니다. 처음에 첨자 연산자를 구현할 때와 같이 참조 사용을 깨달았습니다. 나는 이것이 사실이라고 믿는다.
Nick Bolton

이상한 일이지만, 당신이 구현할 수있는 operator[]참조를 사용하지 않고 컨테이너 ... 그리고 std::vector<bool>않습니다. (그리고 프로세스에서 진짜 혼란을 만듭니다)
Ben Voigt

@BenVoigt mmm, 왜 엉망입니까? 프록시를 반환하는 것은 ::std::vector<bool>언급 한 것처럼 외부 유형에 직접 매핑되지 않는 복잡한 저장소가있는 컨테이너에 유효한 시나리오입니다 .
Sergey.quixoticaxis.Ivanov

1
@ Sergey.quixoticaxis.Ivanov : 다른 인스턴스화와 동작이 매우 다르기 때문에 std::vector<T>템플릿 코드에서 사용 이 중단 될 T수 있습니다 . 유용하지만 고유 한 이름을 지정해야하고의 전문화가되어서는 안됩니다 . boolstd::vector<bool>std::vector
Ben Voigt

@ BenVoight 나는 하나의 전문화를 "정말 특별하게"만드는 이상한 결정에 대한 요점에 동의하지만, 귀하의 원래 의견은 프록시 반환이 일반적으로 이상하다는 것을 암시한다고 생각했습니다.
Sergey.quixoticaxis.Ivanov

2

허용 된 답변에 추가 :

struct immutableint {
    immutableint(int i) : i_(i) {}

    const int& get() const { return i_; }
private:
    int i_;
};

나는이 예가 좋지 않으며 가능한 경우 피해야 한다고 주장 합니다. 왜? 매달려있는 참조 로 끝내기가 매우 쉽습니다 .

예를 들어 요점을 설명하려면 :

struct Foo
{
    Foo(int i = 42) : boo_(i) {}
    immutableint boo()
    {
        return boo_;
    }  
private:
    immutableint boo_;
};

위험 구역에 들어가기 :

Foo foo;
const int& dangling = foo.boo().get(); // dangling reference!

1

반환 참조는 일반적으로 큰 객체의 C ++에서 연산자 오버로드에 사용됩니다. 값을 반환하면 복사 작업이 필요하기 때문입니다 (perator 오버로드에서는 일반적으로 포인터를 반환 값으로 사용하지 않습니다)

그러나 반환 참조는 메모리 할당 문제를 일으킬 수 있습니다. 결과에 대한 참조는 반환 값에 대한 참조로 함수에서 전달되므로 반환 값은 자동 변수가 될 수 없습니다.

반환 참조를 사용하려면 정적 객체 버퍼를 사용할 수 있습니다. 예를 들어

const max_tmp=5; 
Obj& get_tmp()
{
 static int buf=0;
 static Obj Buf[max_tmp];
  if(buf==max_tmp) buf=0;
  return Buf[buf++];
}
Obj& operator+(const Obj& o1, const Obj& o1)
{
 Obj& res=get_tmp();
 // +operation
  return res;
 }

이런 식으로 반환 기준을 안전하게 사용할 수 있습니다.

그러나 functiong에서 값을 반환하기 위해 항상 참조 대신 포인터를 사용할 수 있습니다.


0

함수의 반환 값으로 참조를 사용하는 것이 포인터를 함수의 반환 값으로 사용하는 것보다 훨씬 간단하다고 생각합니다. 둘째, 반환 값이 참조하는 정적 변수를 사용하는 것이 항상 안전합니다.


0

가장 좋은 것은 객체를 만들고이 변수를 할당하는 함수에 참조 / 포인터 매개 변수로 전달하는 것입니다.

함수에 객체를 할당하고 참조 또는 포인터로 포인터를 반환하는 것은 (포인터가 더 안전합니다) 함수 블록 끝에서 메모리를 비우기 때문에 나쁜 생각입니다.


-1
    Class Set {
    int *ptr;
    int size;

    public: 
    Set(){
     size =0;
         }

     Set(int size) {
      this->size = size;
      ptr = new int [size];
     }

    int& getPtr(int i) {
     return ptr[i];  // bad practice 
     }
  };

getPtr 함수는 삭제 후 또는 심지어 널 오브젝트로 동적 메모리에 액세스 할 수 있습니다. 잘못된 액세스 예외가 발생할 수 있습니다. 대신 getter 및 setter를 구현하고 크기를 확인한 후 반환해야합니다.


-2

lvalue로서의 함수 (일명, 상수가 아닌 참조의 반환)는 C ++에서 제거해야합니다. 매우 직관적이지 않습니다. Scott Meyers는이 동작으로 min ()을 원했습니다.

min(a,b) = 0;  // What???

이것은 실제로 개선되지 않습니다

setmin (a, b, 0);

후자는 더 의미가 있습니다.

나는 lvalue로서의 함수가 C ++ 스타일 스트림에 중요하다는 것을 알고 있지만 C ++ 스타일 스트림은 끔찍하다는 것을 지적 할 가치가 있습니다. 나는 이것을 생각하는 유일한 사람이 아닙니다 ... Alexandrescu가 더 나은 방법에 대한 큰 기사를 보았을 때 부스트가 더 나은 유형의 안전한 I / O 방법을 만들려고 노력했다고 생각합니다.


2
물론 위험하고 더 나은 컴파일러 오류 검사가 필요하지만 std :: map의 operator [] ()와 같은 유용한 구문을 수행 할 수 없습니다.
j_random_hacker

2
비 const 참조를 반환하는 것은 실제로 매우 유용합니다. vector::operator[]예를 들어. 당신은 오히려 쓴다 v.setAt(i, x)v[i] = x? 후자는 FAR보다 우수합니다.
Miles Rout

1
@MilesRout 언제든 갈 것입니다 v.setAt(i, x). FAR이 우수합니다.
scravy

-2

나는 그것이 실제로 악한 실제 문제에 부딪쳤다. 본질적으로 개발자는 벡터의 객체에 대한 참조를 반환했습니다. 그것은 나빴다! !!

내가 Janurary에서 쓴 전체 세부 사항 : http://developer-resource.blogspot.com/2009/01/pros-and-cons-of-returing-references.html


2
호출 코드에서 원래 값을 수정해야하는 경우 심판을 반환 해야 합니다. 그리고 그것은 반복자를 벡터로 반환하는 것보다 더 위험하지 않으며, 요소가 벡터에 추가되거나 벡터에서 제거되면 둘 다 무효화됩니다.
j_random_hacker

이 특정 문제는 벡터 요소에 대한 참조를 보유한 다음 참조를 무효화하는 방식으로 해당 벡터를 수정함으로써 발생합니다. "C ++ 표준 라이브러리 : 자습서 및 참조"섹션 6.2-Josuttis는 다음과 같이 읽습니다. 또는 요소를 제거하면 다음 요소를 참조하는 참조, 포인터 및 반복자가 무효화됩니다. 삽입으로 인해 재 할당이 발생하면 모든 참조, 반복자 및 포인터가 무효화됩니다. "
Trent

-15

끔찍한 코드 정보 :

int& getTheValue()
{
   return *new int;
}

따라서 실제로 반환 후 메모리 포인터가 손실되었습니다. 그러나 shared_ptr을 다음과 같이 사용하면 :

int& getTheValue()
{
   std::shared_ptr<int> p(new int);
   return *p->get();
}

반환 후 메모리가 손실되지 않으며 할당 후에 해제됩니다.


12
공유 포인터가 범위를 벗어나고 정수를 해제하기 때문에 손실됩니다.

포인터가 손실되지 않으면 참조 주소가 포인터입니다.
dgsomerton
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.