方法

时间:2018-06-09 03:29:42

标签: c++ inline one-definition-rule

有时,我想在头文件中包含一个函数(包含在多个不同的转换单元中),而不告诉编译器内联它(例如在一个只有头的库中)。使用C风格的方式很容易,只需声明函数static,例如:

   struct somedata { ... }

   static somefunc (somedata *self) { ... }

这使得编译器可以决定是否内联函数,同时仍允许多个翻译单元中的函数的多个定义,因为它没有外部链接。并且还允许我使用在其他翻译单元中创建的somedata结构调用此函数,因为类型是兼容的。

我的问题是,如何使用类和方法执行此操作?例如,使用此头文件,使用类和方法而不是函数和显式对象指针实际上是相同的事情:

   struct someclass {
      void method ();
   }

   void someclass::method () { ... }

显然,我无法使用static someclass::method,因为这完全不同。

我也不能将它放入匿名命名空间,因为我在不同的翻译单元中得到不同的struct someclass类型,即我不能在另一个文件中使用someclass *,因为它们将是不同的(和不兼容的)类型。

我可以声明所有这些方法inline,这些方法可行,但是会产生不良影响,好吧,要求编译器内联它们即使没有意义。

我是否遗漏了一些明显的东西,或者C ++没有相应的static方法?正如我所看到的(希望是错误的),对我来说唯一的选择是将所有这些方法移动到一个单独的翻译单元中,或者将它们全部标记为内联 - 似乎没有相当于C风格的内部链接。

更新:我认为此问题过早关闭,因为重复。所谓的重复问题是关于标记函数是否内联将始终内联它们。这个问题是关于如何避免ODR规则作为普通函数的静态关键字,或解释这不能在C ++中完成,其他问题都没有解答,这只是告诉提问者不要担心它。在我的问题中,内联仅被提及为可能(但不好)的解决方案。

更新2:多次提到内联不是函数内联请求,或者C标准仅使用内联来绕过ODR规则并且不询问编译器内联函数。

这两种说法显然都是不真实的。例如,仔细阅读GCC文档或LLVM源代码可以发现,广泛使用的编译器会将inline视为内联函数的请求。我还引用了C ++ 03,它说(在7.1.2.2中):

[...]内联说明符指示实现在调用点处函数体的内联替换优先于通常的函数调用机制。 [...]

所以现有的编译器和C ++标准(我只检查过148882:2003)显然不同意重复声明的内容,而内联只影响ODR"。

这种错误的看法似乎相当普遍,例如,这里:https://blog.tartanllama.xyz/inline-hints/有人通过查看实际的GCC / LLVM源代码来调查此声明,并发现两个编译器都将内联视为实际的内联请求。

但是,请记住,我的问题是如何在C ++中为成员函数获取static的效果,或者为了获得更加明确的声明,即C ++根本没有这个功能成员函数,仅用于普通函数。 inline的属性仅与此相关,因为它以牺牲可能不需要的内联为代价来解决问题,这可能对性能有害。

对我而言,相对清楚的是,没有办法绕过一个定义规则。我不清楚的是,是否真的没有办法实现这种效果。例如,static的下一个最好的东西是匿名命名空间,但它也不起作用,因为它使得在其中声明的结构在不同的翻译单元之间都不兼容,因此它们不能互换。

我希望可能有办法绕过它,例如,将结构放在匿名命名空间之外,并在内部有一个派生类,或者其他一些构造,但是我现在看不清楚如何与此同时,我不能排除它可能 - 因此这个问题。

更新3 :为了澄清示例 - 方法不包含静态变量,最终结果是否导致方法的多个物理上不同的副本并不重要,只要所有这些副本的行为相同。这种方法的一个实际例子是:

char *reserve (int bytes)
{
  if (left <= bytes)
    flush ();

  if (left <= bytes)
    throw std::runtime_error ("bulkbuf allocation overflow");

  return cur;
}

如果经常调用此方法(在源代码中),但不经常调用(在运行时),请求编译器无缘无故地内联它可能会对性能和代码大小产生不利影响。

更新4 :尽管有充分的证据证明这是不正确的,但仍有许多人反复声称编译器会普遍忽略内联关键字作为内联请求。

为了摆脱任何疑虑,我尝试了这个(相当荒谬的)程序:

//inline
int f(int i)
{   
  return i < 0 ? 0 : f(i-1) + 1;
} 

int main(int argc, char *[])
{   
  return f(5) + f(argc);
} 

请注意已注释掉的inline关键字。

当我使用来自Debian GNU / Linux Stretch的g ++ 6.3.0(2016年发布)编译它时(使用g++ -Os -S -o - test.C),当main被注释掉时,我会得到这个inline程序:

    movl    %edi, %ecx
    movl    $5, %edi
    call    f(int)
    movl    %eax, %edx
    movl    %ecx, %edi
    call    f(int)
    addl    %edx, %eax
    ret

当内联在以下情况下有效:

        xorl    %eax, %eax
.L3:
        cmpl    %eax, %edi
        js      .L2
        incl    %eax
        jmp     .L3
.L2:
        addl    $6, %eax
        ret

因此,如果没有inline函数没有内联inline,它就会被内联。至少,这证明编译器并不像通常所声称的那样普遍忽略内联作为内联请求(并且g ++肯定是少数几个主要的C ++编译器之一,并且版本6.3几乎不会过时,所以这不是一个奇怪的利基编译器。)

事实上,标准编译器和现有编译器都将内联视为不仅仅是ODR行为更改,即作为内联函数的显式请求。

请注意,编译器是否忽略提示只与我的问题相关,这与C ++语言有关,而不是任何编译器,并且至少C ++ 03要求编译器优先&#34;优先&#34;内联函数标记为这样,不要求它们这样做,因此我对内联的关注是否有效,无论编译器是否忽略它。

更新5:

f更改为:

返回i&lt; 0? 1:f(i-1)+ f(i-2);

导致clang ++ 3.8.1-24和g ++的模拟行为。此外,Do c++11-compatible compilers always ignore inline hints?声称MSVC还将内联关键字作为实际内联请求。

g ++,clang ++ / LLVM和MSVC共同占据了C ++&#34;市场的大部分份额,因此可以肯定地说编译器几乎普遍将内联视为内联请求,无论他们是否注意它与否,以及C ++标准的其他要求。

2 个答案:

答案 0 :(得分:2)

非成员函数的staticinline的语义不同,即使函数定义相同也是如此。

// in several translation units
static void foo_static() { 
   static int bar; // one copy per translation unit
}

// in several translation units
inline void foo_inline() { 
   static int bar; // one copy in the entire program
}
翻译单位的

&foo_static也会有所不同,而&foo_inline则相同。

无法为成员函数请求static语义(即使对于静态成员函数)。

如果没有实际声明它(显式或隐式)inline,也无法为任何函数请求inline语义。换句话说,除了实际的内联之外,没有办法说“使这个函数在所有内容中都像inline”。

另一方面,函数 templates 的语义类似于inline函数的语义而没有向编译器请求(现在无论如何都没有意义)在它们的调用站点内联它们

// in several translation units
template <nullptr_t=nullptr>
void foo_template() { 
   static int bar; // one copy in the entire program
}

答案 1 :(得分:1)

标记inline

如果您标记函数static,您将在包含该标题的每个翻译单元中获得该函数的单独副本;因此,您将在可执行文件中拥有该功能的多个副本。如果你将它标记为inline并且编译器没有内联展开它,你将在可执行文件中获得它的一个副本,无论有多少翻译单元包含该标题。