C 또는 C ++로 구조체를 반환하는 것이 안전합니까?


85

내가 이해하는 것은 이것이 수행되어서는 안된다는 것입니다.하지만 나는 이와 같은 일을하는 예제를 보았다고 믿습니다 (코드가 반드시 구문 적으로 정확하지는 않지만 아이디어가 있습니다)

typedef struct{
    int a,b;
}mystruct;

그리고 여기에 함수가 있습니다.

mystruct func(int c, int d){
    mystruct retval;
    retval.a = c;
    retval.b = d;
    return retval;
}

나는 우리가 이와 같은 일을하고 싶다면 항상 malloc 구조체에 대한 포인터를 반환해야한다는 것을 이해했지만, 이런 일을하는 예제를 본 적이 있다고 확신한다. 이 올바른지? 개인적으로 나는 항상 malloc의 구조체에 대한 포인터를 반환하거나 함수에 대한 참조로 전달하고 거기에서 값을 수정합니다. (내 이해는 함수의 범위가 끝나면 구조를 할당하는 데 사용 된 스택을 덮어 쓸 수 있다는 것입니다).

질문에 두 번째 부분을 추가해 보겠습니다. 컴파일러에 따라 다를까요? 그렇다면 데스크톱 용 최신 버전의 컴파일러 gcc, g ++ 및 Visual Studio의 동작은 무엇입니까?

문제에 대한 생각?


34
"내가 이해하는 것은 이것이 이루어져서는 안된다는 것"이라고 누가 말한다? 나는 항상 그것을하고있다. 또한 typedef는 C ++에서 필요하지 않으며 "C / C ++"와 같은 것은 존재하지 않습니다.
PlasmaHH 2011 년

4
질문은 C ++를 대상 으로 하지 않는 것 같습니다 .
Captain Giraffe

4
@PlasmaHH 큰 구조를 복사하는 것은 비효율적 일 수 있습니다. 그렇기 때문에 구조를 값으로 반환하기 전에 신중하고 열심히 생각해야합니다. 특히 구조에 값 비싼 복사 생성자가 있고 컴파일러가 반환 값 최적화에 좋지 않은 경우에는 더욱 그렇습니다. 최근에 한 프로그래머가 모든 곳에서 값으로 반환하기로 결정한 몇 가지 큰 구조에 대한 복사 생성자에 상당한 시간을 소비하는 앱을 최적화했습니다. 비 효율성으로 인해 구매해야하는 추가 데이터 센터 하드웨어에 약 $ 800,000의 비용이 들었습니다.
Crashworks

8
@Crashworks : 축하합니다. 상사가 인상을 주셨기를 바랍니다.
PlasmaHH 2011 년

6
@Crashworks : 생각하지 않고 항상 가치로 반환하는 것은 좋지 않지만 자연스러운 상황에서는 일반적으로 복사본을 만들 필요가없는 안전한 대안이 없으므로 가치로 반환하는 것이 최선의 해결책입니다. 힙 할당이 필요하지 않습니다. 종종 심지어이되지 않습니다 가능 때와 C ++ 11에서 점프해야 좋은 컴파일러 복사 생략을 사용하여 복사, 이동의 의미는 더욱 깊은 복사 제거 할 수 있습니다. 두 메커니즘은 아무것도 할 경우 제대로 작동하지 않습니다 다른 사람을 하지만 값으로 돌아갑니다.
leftaroundabout 2012 년

답변:


78

완벽하게 안전하며 그렇게하는 것은 잘못이 아닙니다. 또한 컴파일러에 따라 다르지 않습니다.

일반적으로 (예와 같이) 구조체가 너무 크지 않은 경우이 접근 방식이 malloc 구조를 반환하는 것보다 훨씬 낫다고 주장합니다 ( malloc비용이 많이 드는 작업입니다).


3
필드 중 하나가 char *이면 여전히 안전합니까? 이제 구조체에 포인터가있을 것입니다
jzepeda

3
@ user963258은 실제로 복사 생성자와 소멸자를 구현하는 방법에 따라 다릅니다.
Luchian Grigore

2
@PabloSantaCruz 까다로운 질문입니다. 시험 문제인 경우 소유권을 고려해야 할 경우 시험관은 응답으로 "아니오"를 예상 할 수 있습니다.
Captain Giraffe

2
@CaptainGiraffe : 사실. OP는 이것을 명확히하지 않았고 그의 예제는 기본적으로 C 였기 때문에 C ++ 질문보다 C 질문 에 더 가깝다고 생각했습니다 .
Pablo Santa Cruz

2
@Kos 일부 컴파일러에는 NRVO가 없습니까? 몇 년부터? 또한 참고 : C ++ 11에서는 NRVO가 없더라도 이동 의미 체계를 대신 호출합니다.
데이비드

73

완벽하게 안전합니다.

당신은 가치로 돌아오고 있습니다. 정의되지 않은 동작으로 이어지는 것은 참조로 반환하는 경우입니다.

//safe
mystruct func(int c, int d){
    mystruct retval;
    retval.a = c;
    retval.b = d;
    return retval;
}

//undefined behavior
mystruct& func(int c, int d){
    mystruct retval;
    retval.a = c;
    retval.b = d;
    return retval;
}

스 니펫의 동작은 완벽하게 유효하고 정의되어 있습니다. 컴파일러에 따라 다르지 않습니다. 괜찮아!

개인적으로 나는 항상 malloc 구조에 대한 포인터를 반환합니다.

해서는 안됩니다. 가능하면 동적으로 할당 된 메모리를 피해야합니다.

또는 함수에 대한 참조로 전달하고 값을 수정하십시오.

이 옵션은 완벽하게 유효합니다. 선택의 문제입니다. 일반적으로 원래 구조체를 수정하는 동안 함수에서 다른 것을 반환하려면이 작업을 수행합니다.

내 이해는 함수의 범위가 끝나면 구조를 할당하는 데 사용 된 스택을 덮어 쓸 수 있다는 것입니다.

이것은 잘못된 것입니다. 내 말은, 일종의 정확하지만 함수 내부에서 생성 한 구조의 복사본을 반환합니다. 이론적으로 . 실제로 RVO 는 발생할 수 있으며 발생할 수 있습니다. 반환 값 최적화에 대해 읽어보십시오. 즉 retval, 함수가 종료 될 때 범위를 벗어나는 것처럼 보이지만 실제로는 추가 복사를 방지하기 위해 호출 컨텍스트에 빌드 될 수 있습니다. 이것은 컴파일러가 자유롭게 구현할 수있는 최적화입니다.


3
RVO를 언급하면 ​​+1. 이 중요한 최적화는 실제로 STL 컨테이너와 같이 값 비싼 복사 생성자가있는 객체에 대해이 패턴을 실현 가능하게합니다.
Kos

1
컴파일러가 반환 값 최적화를 자유롭게 수행 할 수 있지만 그렇게 할 것이라는 보장은 없습니다. 이것은 당신이 의지 할 수있는 것이 아니라 희망일뿐입니다.
Watcom 2013 년

-1은 "가능한 경우 동적으로 할당 된 메모리를 피합니다." 이것은 newb 규칙 및 코드에서 자주 결과 경향이 LARGE 간단한 포인터가 많은 시간을 절약 할 수 있습니다 때 데이터의 양이 반환됩니다 (사물이 느리게 실행하는 이유가 퍼즐). 올바른 규칙은 속도에 따라 수익 구조 또는 포인터입니다 , 사용법 및 명확성 .
Lloyd Sargent

10

mystruct함수 에서 객체 의 수명 은 실제로 함수를 떠날 때 끝납니다. 그러나 return 문에서 값으로 개체를 전달합니다. 이것은 객체가 함수에서 호출 함수로 복사됨을 의미합니다. 원본 개체는 사라지지만 복사본은 계속 유지됩니다.


9

structC에서 a를 반환하는 것이 안전 할뿐만 아니라 (또는 classC ++에서 struct-s는 실제로 class기본 public:멤버 가있는 -es 임), 많은 소프트웨어가이를 수행합니다.

물론 classC ++에서 a 를 반환 할 때 언어는 일부 소멸자 또는 이동 생성자가 호출되도록 지정하지만 컴파일러에 의해 최적화 될 수있는 경우가 많습니다.

또한 Linux x86-64 ABIstruct두 개의 스칼라 (예 : 포인터 또는 long) 값 으로 a를 반환하는 작업이 레지스터 ( %rax& %rdx)를 통해 수행 되도록 지정하므로 매우 빠르고 효율적입니다. 따라서이 특별한 경우에는 이러한 2- 스칼라 필드를 반환하는 것이 더 빠를 것입니다.struct 다른 작업을 수행하는 것보다 것입니다 (예 : 인수로 전달 된 포인터에 저장).

그런 두 스칼라 필드를 반환하는 struct것은 malloc-ing하고 포인터를 반환하는 것보다 훨씬 빠릅니다 .


5

그것은 완벽하게 합법적이지만 큰 구조체의 경우 고려해야 할 두 가지 요소, 즉 속도와 스택 크기가 있습니다.


1
반환 값 최적화에 대해 들어 보셨습니까?
Luchian Grigore

예, 그러나 우리는 일반적으로 값으로 구조체를 반환하고 있으며 컴파일러가 RVO를 수행 할 수없는 경우가 있습니다.
ebutusov 2012 년

3
나는 당신이 약간의 프로파일 링을 한 후에 여분의 사본에 대해서만 걱정한다고 말하고 싶습니다.
Luchian Grigore

4

구조 유형은 함수가 반환하는 값의 유형일 수 있습니다. 컴파일러가 구조체의 복사본을 만들고 함수의 로컬 구조체가 아닌 복사본을 반환하기 때문에 안전합니다.

typedef struct{
    int a,b;
}mystruct;

mystruct func(int c, int d){
    mystruct retval;
    cout << "func:" <<&retval<< endl;
    retval.a = c;
    retval.b = d;
    return retval;
}

int main()
{
    cout << "main:" <<&(func(1,2))<< endl;


    system("pause");
}

4

안전성은 구조체 자체가 어떻게 구현되었는지에 달려 있습니다. 비슷한 것을 구현하는 동안이 질문에 우연히 발견되었으며 여기에 잠재적 인 문제가 있습니다.

컴파일러는 값을 반환 할 때 몇 가지 작업을 수행합니다.

  1. 복사 생성자를 호출합니다 mystruct(const mystruct&)( 컴파일러 자체에 의해 할당 된 함수 외부this임시 변수 ).func
  2. ~mystruct내부에 할당 된 변수에 대해 소멸자 를 호출합니다.func
  3. mystruct::operator=반환 된 값이 다른 것에 할당되면 호출 합니다.=
  4. ~mystruct컴파일러가 사용하는 임시 변수 에서 소멸자 를 호출합니다.

이제 경우가 mystruct여기에 설명 된 것과 간단하다 모든 괜찮지 만, 그것은 포인터 (같은 경우 char*) 또는 더 복잡한 메모리 관리를, 다음 모든 방법에 따라 달라집니다 mystruct::operator=, mystruct(const mystruct&)그리고 ~mystruct구현됩니다. 따라서 복잡한 데이터 구조를 값으로 반환 할 때주의해야합니다.


C ++ 11 이전에만 해당됩니다.
Björn Sundin

4

당신이 한 것처럼 구조체를 반환하는 것은 완벽하게 안전합니다.

그러나이 진술을 기반으로 : 내 이해는 함수의 범위가 끝나면 구조를 할당하는 데 사용 된 스택을 덮어 쓸 수 있다는 것이므로 구조 의 멤버가 동적으로 할당 된 시나리오 만 상상할 수 있습니다 ( malloc'ed 또는 new'ed),이 경우 RVO가 없으면 동적으로 할당 된 멤버가 삭제되고 반환 된 복사본에 가비지를 가리키는 멤버가 있습니다.


스택은 복사 작업을 위해 일시적으로 만 사용됩니다. 일반적으로 스택은 호출 전에 예약되고 호출 된 함수는 반환 될 데이터를 스택에 넣은 다음 호출자는 스택에서이 데이터를 가져와 할당 된 위치에 저장합니다. 따라서 걱정할 필요가 없습니다.
Thomas Tempelmann 2012 년

3

나는 또한 sftrabbit에 동의 할 것입니다. Life는 실제로 끝나고 스택 영역이 정리되지만 컴파일러는 모든 데이터가 레지스터 또는 다른 방법으로 검색되어야 할만큼 충분히 똑똑합니다.

확인을위한 간단한 예가 아래에 나와 있습니다. (Mingw 컴파일러 어셈블리에서 가져옴)

_func:
    push    ebp
    mov ebp, esp
    sub esp, 16
    mov eax, DWORD PTR [ebp+8]
    mov DWORD PTR [ebp-8], eax
    mov eax, DWORD PTR [ebp+12]
    mov DWORD PTR [ebp-4], eax
    mov eax, DWORD PTR [ebp-8]
    mov edx, DWORD PTR [ebp-4]
    leave
    ret

b의 값이 edx를 통해 전송되었음을 알 수 있습니다. 기본 eax에는 a에 대한 값이 포함되어 있습니다.


2

구조를 반환하는 것은 안전하지 않습니다. 내가 직접하는 것을 좋아하지만 누군가가 나중에 반환 된 구조체에 복사 생성자를 추가하면 복사 생성자가 호출됩니다. 이것은 예상치 못한 일이며 코드가 손상 될 수 있습니다. 이 버그는 찾기가 매우 어렵습니다.

나는 더 정교하게 대답했지만 사회자는 그것을 좋아하지 않았다. 따라서 귀하의 비용으로 제 팁은 짧습니다.


“구조물을 반환하는 것은 안전하지 않습니다. […] 복사 생성자가 호출됩니다.” – 안전비효율 사이에는 차이가 있습니다 . 구조체를 반환하는 것은 확실히 안전합니다. 그럼에도 불구하고, 호출자의 스택에서 구조체가 시작될 때 생성되기 때문에 복사 ctor에 대한 호출은 컴파일러에 의해 제거 될 가능성이 높습니다.
phg

2

질문에 두 번째 부분을 추가해 보겠습니다. 컴파일러에 따라 다를까요?

실제로 내 고통을 발견했듯이 http://sourceforge.net/p/mingw-w64/mailman/message/33176880/

win32 (MinGW)에서 gcc를 사용하여 구조체를 반환하는 COM 인터페이스를 호출했습니다. MS가 GNU와 다르게 수행하므로 내 (gcc) 프로그램이 스택과 함께 충돌했습니다.

MS가 여기서 더 높은 지위를 가질 수도 있지만 내가 신경 쓰는 것은 Windows에서 빌드하기위한 MS와 GNU 간의 ABI 호환성뿐입니다.

그렇다면 데스크톱 용 최신 버전의 컴파일러 gcc, g ++ 및 Visual Studio의 동작은 무엇입니까?

Wine 메일 링리스트에서 MS가 어떻게 보이는지에 대한 메시지를 찾을 수 있습니다.


당신이 언급하고있는 Wine 메일 링리스트에 포인터를 주면 더 도움이 될 것입니다.
Jonathan Leffler

구조체 반환은 괜찮습니다. COM은 바이너리 인터페이스를 지정합니다. 누군가 COM을 제대로 구현하지 않으면 버그가 될 것입니다.
MM

1

참고 :이 답변은 C ++ 11 이후에만 적용됩니다. "C / C ++"와 같은 것은 없으며 서로 다른 언어입니다.

아니요, 로컬 개체를 값으로 반환하는 것은 위험하지 않으므로 그렇게하는 것이 좋습니다. 그러나 여기에 모든 답변에서 빠진 중요한 점이 있다고 생각합니다. 다른 많은 사람들은 구조체가 RVO를 사용하여 복사되거나 직접 배치되고 있다고 말했습니다. 그러나 이것은 완전히 정확하지 않습니다. 나는 지역 객체를 반환 할 때 어떤 일이 일어날 수 있는지 정확하게 설명하려고 노력할 것입니다.

의미론 이동

C ++ 11 이후로 안전하게 훔칠 수있는 임시 객체에 대한 참조 인 rvalue 참조가 있습니다. 예를 들어 std :: vector에는 이동 할당 연산자와 함께 이동 생성자가 있습니다. 둘 다 일정한 복잡도를 가지며 단순히 이동되는 벡터의 데이터에 대한 포인터를 복사합니다. 여기서는 이동 의미론에 대해 자세히 설명하지 않겠습니다.

함수 내에서 로컬로 생성 된 객체는 임시적이고 함수가 반환 될 때 범위를 벗어나기 때문에 반환 된 객체는 C ++ 11 이후부터 복사 되지 않습니다 . 반환되는 객체에서 이동 생성자가 호출됩니다 (또는 나중에 설명 됨). 즉, 값 비싼 복사 생성자를 사용하여 객체를 반환하지만 큰 벡터와 같이 저렴한 이동 생성자를 사용하면 데이터의 소유권 만 로컬 객체에서 반환 된 객체로 전송되므로 저렴합니다.

특정 예에서는 개체 복사와 이동간에 차이가 없습니다. 구조체의 기본 이동 및 복사 생성자는 동일한 작업을 수행합니다. 두 개의 정수를 복사합니다. 그러나 이것은 전체 구조체가 64 비트 CPU 레지스터에 맞기 때문에 적어도 다른 솔루션보다 빠릅니다. (내가 틀리면 CPU 레지스터를 많이 알지 못합니다).

RVO 및 NRVO

RVO는 반환 값 최적화를 의미하며 컴파일러가 수행하는 몇 안되는 최적화 중 하나이며 부작용이있을 수 있습니다. C ++ 17부터 RVO가 필요합니다. 이름이 지정되지 않은 개체를 반환 할 때 호출자가 반환 된 값을 할당하는 제자리에서 직접 생성됩니다. 복사 생성자 또는 이동 생성자가 호출되지 않습니다. RVO가 없으면 이름이 지정되지 않은 객체가 먼저 로컬에서 생성 된 다음 반환 된 주소에서 생성 된 다음 이동하면 이름이 지정되지 않은 로컬 객체가 파괴됩니다.

RVO가 필요하거나 (c ++ 17) 가능성이있는 예 (c ++ 17 이전) :

auto function(int a, int b) -> MyStruct {
    // ...
    return MyStruct{a, b};
}

NRVO는 명명 된 반환 값 최적화를 의미하며 호출 된 함수에 로컬 인 명명 된 개체에 대해 수행된다는 점을 제외하면 RVO와 동일합니다. 이것은 여전히 ​​표준 (c ++ 20)에 의해 보장되지 않지만 많은 컴파일러가 여전히 그렇게합니다. 명명 된 로컬 개체를 사용하더라도 반환 될 때 최악의 경우 이동됩니다.

결론

값으로 반환하지 않는 것을 고려해야하는 유일한 경우는 명명 된 매우 큰 (스택 크기에서와 같이) 객체가있는 경우입니다. 이는 NRVO가 아직 보장되지 않았기 때문입니다 (c ++ 20 기준). 객체 이동조차 느려질 수 있기 때문입니다. 내 권장 사항과 Cpp 핵심 지침 의 권장 사항은 항상 값으로 개체를 반환하는 것을 선호하는 것입니다 (복수 값이 여러 개인 경우 구조체 (또는 튜플) 사용). 유일한 예외는 개체를 이동하는 데 비용이 많이 드는 경우입니다. 이 경우 상수가 아닌 참조 매개 변수를 사용하십시오.

C ++의 함수에서 수동으로 해제해야하는 리소스를 반환하는 것은 결코 좋은 생각이 아닙니다. 그렇게하지 마십시오. 최소한 std :: unique_ptr을 사용하거나 리소스 ( RAII ) 를 해제하고 그 인스턴스를 반환하는 소멸자로 자신의 비 로컬 또는 로컬 구조체를 만드십시오 . 리소스에 자체 이동 의미가없는 경우 이동 생성자와 이동 할당 연산자를 정의하는 것도 좋은 생각입니다 (복사 생성자 / 할당 삭제).


흥미로운 사실. 저는 golang이 비슷한 것을 가지고 있다고 생각합니다.
Mox
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.