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.