-1

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

   struct somedata { ... }

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

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

我的问题是,如何使用类和方法来做到这一点?例如,以这个头文件为例,它实际上是使用类和方法而不是函数和显式对象指针的同一件事:

   struct someclass {
      void method ();
   }

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

显然,我不能使用static someclass::method,因为那完全是另外一回事。

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

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

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

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

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

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

[...] inline 说明符向实现表明,在调用点对函数体进行内联替换优于通常的函数调用机制。[...]

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

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

但是,请记住,我的问题是关于如何static在 C++ 中获得 for 成员函数的效果,或者获得更明确的声明,即 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 关键字作为内联请求。

为了消除任何疑问,我用这个(相当荒谬的)程序进行了尝试:

//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

当 inline 处于活动状态时:

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

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

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

请注意,编译器是否忽略提示仅与我的问题相关,即关于 C++ 语言,而不是任何编译器,并且至少 C++03 要求编译器“优先”标记为这样的内联函数,而不需要他们这样做,所以无论编译器是否忽略它,我对 inline 的担忧都是有效的。

更新 5:

更改f为:

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

导致 clang++ 3.8.1-24 和 g++ 的模拟行为。此外,兼容 c++11 的编译器是否总是忽略内联提示?声称 MSVC 还将 inline 关键字作为实际内联的请求。

g++、clang++/LLVM 和 MSVC 一起占据了 C++“市场”的很大一部分,因此可以肯定地说,编译器几乎普遍将 inline 视为内联请求,无论他们是否注意到它,以及除了来自C++ 标准。

4

2 回答 2

2

即使函数定义在其他方面相同,非成员函数的语义也static不同。inline

// 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除了实际内联之外的所有东西”。

另一方面,函数模板的语义类似于函数的语义,inline而无需向编译器请求(尽管现在它已经毫无意义)在其调用站点内联它们。

// in several translation units
template <nullptr_t=nullptr>
void foo_template() { 
   static int bar; // one copy in the entire program
}
于 2018-06-09T16:01:33.793 回答
1

标记它inline

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

于 2018-06-09T04:05:31.030 回答