인라인 가상 함수가 실제로 의미가 없습니까?


172

가상 기능을 인라인 할 필요가 없다는 코드 검토 의견을 받았을 때이 질문이 있습니다.

인라인 가상 함수는 함수가 객체에서 직접 호출되는 시나리오에서 유용 할 수 있다고 생각했습니다. 그러나 반론은 내 생각에왔다. 왜 가상을 정의하고 객체를 사용하여 메소드를 호출하고 싶을까?

인라인 가상 함수는 거의 확장되지 않으므로 사용하지 않는 것이 가장 좋습니까?

분석에 사용한 코드 스 니펫 :

class Temp
{
public:

    virtual ~Temp()
    {
    }
    virtual void myVirtualFunction() const
    {
        cout<<"Temp::myVirtualFunction"<<endl;
    }

};

class TempDerived : public Temp
{
public:

    void myVirtualFunction() const
    {
        cout<<"TempDerived::myVirtualFunction"<<endl;
    }

};

int main(void) 
{
    TempDerived aDerivedObj;
    //Compiler thinks it's safe to expand the virtual functions
    aDerivedObj.myVirtualFunction();

    //type of object Temp points to is always known;
    //does compiler still expand virtual functions?
    //I doubt compiler would be this much intelligent!
    Temp* pTemp = &aDerivedObj;
    pTemp->myVirtualFunction();

    return 0;
}

1
어셈블러 목록을 얻는 데 필요한 스위치로 예제를 컴파일 한 다음 실제로 컴파일러가 가상 함수를 인라인 할 수있는 코드 검토자를 표시하십시오.
Thomas L Holaday

1
기본 클래스를 돕기 위해 가상 함수를 호출하기 때문에 위의 내용은 일반적으로 인라인되지 않습니다. 컴파일러가 얼마나 똑똑한 지에 달려 있습니다. pTemp->myVirtualFunction()비가 상 통화로 해결할 수 있다고 지적하면 해당 통화가 인라인되었을 수 있습니다. 이 참조 된 호출은 g ++ 3.4.2로 인라인됩니다 TempDerived & pTemp = aDerivedObj; pTemp.myVirtualFunction();. 코드가 아닙니다.
doc

1
gcc가 실제로 수행하는 한 가지는 vtable 항목을 특정 기호와 비교 한 다음 일치하는 경우 루프에서 인라인 변형을 사용하는 것입니다. 인라인 함수가 비어 있고이 경우 루프를 제거 할 수있는 경우에 특히 유용합니다.
Simon Richter

1
@doc 최신 컴파일러는 컴파일 타임에 포인터의 가능한 값을 결정하려고 노력합니다. 포인터를 사용하는 것만으로는 중요한 최적화 수준에서 인라인을 방지 할 수 없습니다. GCC는 최적화 제로에서 단순화를 수행합니다!
curiousguy

답변:


153

가상 기능은 때때로 인라인 될 수 있습니다. 뛰어난 C ++ FAQ 에서 발췌 :

"인라인 가상 호출을 인라인 할 수있는 유일한 시간은 컴파일러가 가상 함수 호출의 대상인 객체의"정확한 클래스 "를 알고있을 때뿐입니다. 이는 컴파일러에 포인터 나 포인터가 아닌 실제 객체가있는 경우에만 발생할 수 있습니다. 로컬 오브젝트, 전역 / 정적 오브젝트 또는 컴포지트 내에 완전히 포함 된 오브젝트가있는 오브젝트를 참조하십시오. "


7
그러나 컴파일 타임에 호출을 해결할 수 있고 인라인 될 수있는 경우에도 컴파일러가 인라인 지정자를 무시해도된다는 점을 기억해야합니다.
sharptooth

6
인라인이 발생할 수 있다고 생각되는 어머님 상황은 예를 들어 this-> Temp :: myVirtualFunction ()과 같은 메소드를 호출하는 경우입니다. 이러한 호출은 가상 테이블 해상도를 건너 뛰고 함수는 문제없이 인라인되어야합니다-왜 그리고 당신이 d 다른 주제입니다 :)
RnR

5
@RnR. 'this->'를 가질 필요는 없으며 규정 된 이름 만 사용하면 충분합니다. 그리고이 동작은 소멸자, 생성자 및 일반적으로 할당 연산자에서 발생합니다 (내 답변 참조).
Richard Corden

2
sharptooth-true이지만 AFAIK는 가상 인라인 함수뿐만 아니라 모든 인라인 함수에서도 마찬가지입니다.
Colen

2
void f (const Base & lhs, const Base & rhs) {} ------ 함수를 구현할 때 런타임까지 lhs와 rhs가 무엇을 가리키는 지 결코 알 수 없습니다.
Baiyan Huang

72

C ++ 11이 추가되었습니다 final. 이것은 허용되는 답변을 변경합니다. 더 이상 객체의 정확한 클래스를 알 필요가 없습니다. 객체에 최소한 함수가 final로 선언 된 클래스 유형이 있다는 것을 아는 것으로 충분합니다.

class A { 
  virtual void foo();
};
class B : public A {
  inline virtual void foo() final { } 
};
class C : public B
{
};

void bar(B const& b) {
  A const& a = b; // Allowed, every B is an A.
  a.foo(); // Call to B::foo() can be inlined, even if b is actually a class C.
}

VS 2017에서 인라인 할 수 없었습니다.
Yola

1
나는 그것이 이런 식으로 작동한다고 생각하지 않습니다. 유형 A의 포인터 / 참조를 통한 foo () 호출은 인라인 될 수 없습니다. b.foo ()를 호출하면 인라인을 허용해야합니다. 컴파일러가 이전 행을 알고 있으므로 B 유형이라는 것을 이미 알고 있다고 제안하지 않는 한. 그러나 그것은 일반적인 사용법이 아닙니다.
Jeffrey Faust

예를 들어, 여기 바, BAS에 대해 생성 된 코드를 비교 : godbolt.org/g/xy3rNh
제프리 파우스트

@JeffreyFaust 정보가 전파되어서는 안될 이유가 없습니까? 그리고 icc그 링크에 따르면 그렇게하는 것 같습니다.
Alexey Romanov

@AlexeyRomanov 컴파일러는 표준을 뛰어 넘어 자유롭게 최적화 할 수 있습니다. 위와 같은 간단한 경우 컴파일러는 유형을 알고이 최적화를 수행 할 수 있습니다. 상황이 이처럼 단순하지는 않으며 컴파일 타임에 다형성 변수의 실제 유형을 결정할 수있는 것은 일반적이지 않습니다. OP는 이러한 특별한 경우가 아니라 '일반적인'것에 관심이 있다고 생각합니다.
Jeffrey Faust

37

가상 함수의 범주는 여전히 인라인으로 유지하는 것이 합리적입니다. 다음과 같은 경우를 고려하십시오.

class Base {
public:
  inline virtual ~Base () { }
};

class Derived1 : public Base {
  inline virtual ~Derived1 () { } // Implicitly calls Base::~Base ();
};

class Derived2 : public Derived1 {
  inline virtual ~Derived2 () { } // Implicitly calls Derived1::~Derived1 ();
};

void foo (Base * base) {
  delete base;             // Virtual call
}

'base'를 삭제하는 호출은 올바른 파생 클래스 소멸자를 호출하기위한 가상 호출을 수행하며이 호출은 인라인되지 않습니다. 그러나 각 소멸자가 부모 소멸자를 호출하므로 (이 경우 비어 있음) 컴파일러는 기본 클래스 함수를 가상으로 호출하지 않기 때문에 해당 호출을 인라인 할 수 있습니다 .

기본 클래스 생성자 또는 파생 구현이 기본 클래스 구현을 호출하는 함수 세트에도 동일한 원칙이 있습니다.


23
빈 괄호가 항상 소멸자가 아무것도하지 않는다는 것을 의미하지는 않습니다. 소멸자는 클래스의 모든 멤버 객체를 기본적으로 파괴하므로 기본 클래스에 몇 개의 벡터가 있으면 빈 괄호에서 많은 작업을 수행 할 수 있습니다!
Philip

14

인라인이 아닌 함수가 전혀 존재하지 않으면 (그리고 헤더 대신 하나의 구현 파일에 정의 된 경우) v-table을 방출하지 않는 컴파일러를 보았습니다. 그들은 missing vtable-for-class-A비슷한 또는 비슷한 것을 던질 것이고 , 당신은 내가했던 것처럼 지옥처럼 혼란 스러울 것입니다.

실제로, 그것은 표준을 준수하지 않지만, 헤더에 (가상 소멸자 인 경우에만) 적어도 하나의 가상 함수를 넣는 것을 고려하여 컴파일러가 해당 장소에서 클래스에 대한 vtable을 생성 할 수 있도록하십시오. 일부 버전에서 발생한다는 것을 알고 있습니다 gcc.

누군가가 언급 한 바와 같이, 인라인 가상 함수는 도움이 될 수 있습니다 가끔 있지만, 그렇게되면 물론 가장 자주 당신은 그것을 사용 하지 개체의 동적 유형을 알고이에 대한 모든 이유 때문에, virtual처음에이.

그러나 컴파일러는 완전히 무시할 수 없습니다 inline. 함수 호출 속도를 높이는 것 외에 다른 의미가 있습니다. 암시 인라인 수준의 정의에 대한 당신이 헤더에 정의를 넣을 수있는 메커니즘입니다 : 만 inline기능을 위반하는 어떤 규칙없이 전체 프로그램 전반에 걸쳐 여러 번 정의 할 수 있습니다. 결국 헤더를 여러 개의 서로 연결된 파일에 여러 번 포함하더라도 전체 프로그램에서 한 번만 정의한 것처럼 작동합니다.


11

음, 사실 가상 함수가 항상 인라인 할 수 있습니다 그들은 정적으로 서로 연결되어있어 길이로, 우리는 추상 클래스가 있다고 가정 Base 가상 기능 F과 파생 클래스 Derived1Derived2:

class Base {
  virtual void F() = 0;
};

class Derived1 : public Base {
  virtual void F();
};

class Derived2 : public Base {
  virtual void F();
};

가상 호출 b->F();( b유형이 있음 Base*)은 사실상 가상입니다. 그러나 당신 (또는 컴파일러 ...)은 그렇게 다시 쓸 수 있습니다 (suppose typeof는에서 typeid사용할 수있는 값을 반환하는 비슷한 함수입니다 switch)

switch (typeof(b)) {
  case Derived1: b->Derived1::F(); break; // static, inlineable call
  case Derived2: b->Derived2::F(); break; // static, inlineable call
  case Base:     assert(!"pure virtual function call!");
  default:       b->F(); break; // virtual call (dyn-loaded code)
}

에 대한 RTTI가 여전히 필요하지만 typeof기본적으로 명령 스트림에 vtable을 포함하고 관련된 모든 클래스에 대한 호출을 전문화하여 호출을 효과적으로 인라인 할 수 있습니다. 이것은 또한 몇 가지 클래스 (예 Derived1: 그냥 ) 를 전문화하여 일반화 될 수 있습니다 .

switch (typeof(b)) {
  case Derived1: b->Derived1::F(); break; // hot path
  default:       b->F(); break; // default virtual call, cold path
}

그들은 이것을하는 컴파일러입니까? 아니면 이것은 단지 추측입니까? 지나치게 회의적이라면 죄송하지만, 위의 설명에서 여러분의 어조는 "일부 컴파일러는이 작업을 수행 할 수 있습니다."
Alex Meiburg

예, GRAAL (또한 Sulong를 통해 LLVM의 비트 코드에 대한) 다형성 인라인을 수행
CAFxX


3

인라인은 실제로 아무것도하지 않습니다-힌트입니다. 컴파일러는이를 무시하거나 구현을보고이 아이디어를 좋아하는 경우 인라인 없이 호출 이벤트를 인라인 할 수 있습니다. 코드 선명도가 문제가되면 인라인 을 제거해야합니다.


2
단일 TU에서만 작동하는 컴파일러의 경우 정의가있는 암시 적으로 함수 만 인라인 할 수 있습니다. 인라인으로 설정하면 여러 TU에서만 기능을 정의 할 수 있습니다. '인라인'은 힌트 이상이며 g ++ / makefile 빌드의 성능이 크게 향상 될 수 있습니다.
Richard Corden

3

인라인 선언 된 가상 함수는 객체를 통해 호출 될 때 인라인되고 포인터 또는 참조를 통해 호출 될 때 무시됩니다.


1

최신 컴파일러를 사용하면 컴파일러를 해치지 않습니다. 일부 고대 컴파일러 / 링커 콤보는 여러 개의 vtable을 만들었을 수도 있지만 더 이상 문제가되지 않습니다.


1

컴파일 타임에 호출을 명확하게 해결할 수있는 경우에만 컴파일러가 함수를 인라인 할 수 있습니다.

그러나 가상 함수는 런타임에 분석되므로 컴파일 유형에서는 동적 유형 (따라서 호출 할 함수 구현)을 판별 할 수 없으므로 컴파일러가 호출을 인라인 할 수 없습니다.


1
동일하거나 파생 된 클래스에서 기본 클래스 메서드를 호출하면 호출이 명확하고 비 가상적입니다.
sharptooth

1
@sharptooth : 그러나 비가 상 인라인 방법입니다. 컴파일러는 요청하지 않은 함수를 인라인 할 수 있으며 인라인 여부를 더 잘 알고있을 것입니다. 결정하자.
David Rodríguez-dribeas

1
@dribeas : 그렇습니다. 정확히 내가 말하는 것입니다. 나는 가상 finctions이 런타임에 해결된다는 진술에만 반대했습니다. 정확한 클래스가 아닌 사실상 호출이 완료된 경우에만 적용됩니다.
sharptooth

나는 말도 안된다고 생각합니다. 크든 작든 가상이든 상관없이 모든 함수를 항상 인라인 할 수 있습니다 . 컴파일러 작성 방법에 따라 다릅니다. 동의하지 않으면 컴파일러가 인라인되지 않은 코드를 생성 할 수 없을 것으로 기대합니다. 즉, 컴파일러는 런타임에 컴파일 타임에 해결할 수없는 조건을 테스트하는 코드를 포함 할 수 있습니다. 현대 컴파일러가 컴파일 타임에 상수 값을 확인하고 숫자 식을 줄일 수있는 것과 같습니다. 함수 / 메소드가 인라인되지 않았다고해서 인라인 될 수 없다는 의미는 아닙니다.

1

함수 호출이 분명하고 함수가 인라인하기에 적합한 후보 인 경우, 컴파일러는 어쨌든 코드를 인라인 할 수있을 정도로 똑똑합니다.

나머지 "인라인 가상"은 의미가 없으며 실제로 일부 컴파일러는 해당 코드를 컴파일하지 않습니다.


인라인 가상을 컴파일하지 않는 g ++ 버전은 무엇입니까?
Thomas L Holaday

흠. 내가 지금 가지고있는 4.1.1은 행복해 보인다. 4.0.x를 사용하여이 코드베이스에서 처음 문제가 발생했습니다. 내 정보가 오래된 것 같아 편집했습니다.
moonshadow

0

가상 함수를 만든 다음 참조 나 포인터가 아닌 객체에서 호출하는 것이 좋습니다. Scott Meyer는 자신의 저서 "유효한 C ++"에서 상속 된 비가 상 함수를 다시 정의하지 말 것을 권장합니다. 비가 상 함수를 사용하여 클래스를 만들고 파생 클래스에서 함수를 재정의 할 때이 클래스를 올바르게 사용하더라도 다른 사용자가 올바르게 사용한다고 확신 할 수는 없습니다. 또한 나중에 잘못 사용할 수 있습니다. 따라서 기본 클래스에서 함수를 만들고이를 재정의 할 수있게하려면 가상으로 만들어야합니다. 가상 함수를 만들고 객체를 호출하는 것이 합리적이라면 인라인하는 것이 좋습니다.


0

실제로 어떤 경우에는 가상 최종 재정의에 "인라인"을 추가하면 코드가 컴파일되지 않으므로 때때로 차이가 있습니다 (적어도 VS2017의 컴파일러에서는)!

실제로 VS2017에서 컴파일 및 링크에 c ++ 17 표준을 추가하여 가상 인라인 최종 재정의 기능을 수행하고 있었고 두 가지 프로젝트를 사용할 때 실패했습니다.

단위 테스트 인 테스트 프로젝트와 구현 DLL이 있습니다. 테스트 프로젝트에 필요한 다른 프로젝트의 * .cpp 파일을 #include하는 "linker_includes.cpp"파일이 있습니다. 알고 있습니다 ... DLL에서 객체 파일을 사용하도록 msbuild를 설정할 수 있지만, cpp 파일을 포함하는 것이 빌드 시스템과 관련이 없으며 버전이 훨씬 더 쉽다는 것을 Microsoft 특정 솔루션이라는 점을 명심하십시오. XML 파일 및 프로젝트 설정 및 기타 CPP 파일 ...

흥미로운 점은 테스트 프로젝트에서 링커 오류가 계속 발생한다는 것입니다. 포함하지 않고 복사 붙여 넣기로 누락 된 함수의 정의를 추가하더라도! 그래서 이상한. 다른 프로젝트는 빌드되었으며 프로젝트 참조를 표시하는 것 이외의 두 프로젝트 사이에는 연결이 없으므로 두 프로젝트가 항상 빌드되도록 빌드 순서가 있습니다 ...

컴파일러에서 일종의 버그라고 생각합니다. VS2020과 함께 제공된 컴파일러에 존재하는지 전혀 모르겠습니다. 일부 SDK는 제대로 작동하기 때문에 이전 버전을 사용하고 있기 때문입니다.

인라인으로 표시하는 것만으로도 의미가있을 수 있지만 드문 상황에서 코드가 작성되지 않을 수도 있습니다. 이것은 이상하지만 알기에 좋습니다.

추신 : 내가 작업중 인 코드는 컴퓨터 그래픽과 관련이 있으므로 인라인을 선호하므로 최종 및 인라인을 모두 사용합니다. 최종 지정자를 유지하여 릴리스 빌드가 DLL을 직접 암시하지 않고도 인라인하여 DLL을 빌드 할만 큼 똑똑해지기를 바랍니다.

PS (Linux) .: 나는 gcc 나 clang에서 이런 종류의 일을 일상적으로 사용했을 때와 같은 일이 일어나지 않을 것으로 기대합니다. 나는이 문제가 어디에서 왔는지 잘 모르겠습니다 ... Linux에서 또는 적어도 일부 gcc로 c ++을 선호하지만 때로는 프로젝트의 요구가 다릅니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.