方法的静态函数等价

Equivalence of static function for methods

有时,我希望在 header 文件(包含在多个不同的翻译单元中)中有一个函数而不告诉编译器将其内联(例如在 header-only 库中)。这在 C-style 方式下很容易,只需声明函数 static,例如:

   struct somedata { ... }

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

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

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

   struct someclass {
      void method ();
   }

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

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

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

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

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

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

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

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

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

所以现有的编译器和C++标准(我只检查了148882:2003)显然不同意"inline only affects ODR"的重复声明。

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

但是,请记住,我的问题是关于如何在 C++ 中为成员函数获得 static 的效果,或者获得更明确的声明,即 C++ 根本没有此功能成员函数,仅适用于普通函数。 inline 的属性仅在它确实以潜在不需要的内联为代价解决问题的范围内才相关,这可能对性能不利。

我比较清楚,没有办法绕过单一定义规则。我不清楚的是是否真的没有其他方法可以达到这种效果。例如,仅次于 static 的是匿名命名空间,但这也不起作用,因为它使其中声明的结构在不同的翻译单元之间完全不兼容,因此它们不能互换。

我希望有办法绕过它,例如,将结构放在匿名命名空间之外,并在内部使用派生的 class 或其他一些构造,但我看不出如何此刻,同时我不能排除它可能的可能性 - 因此这个问题。

更新 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++ -Os -S -o - test.C)的 g++ 6.3.0(2016 年发布)编译它时,我得到这个 main 程序 inline被注释掉:

    movl    %edi, %ecx
    movl    , %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    , %eax
        ret

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

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

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

更新5:

f 更改为:

return 我 < 0 ? 1 : f(i-1) + f(i-2);

导致 clang++ 3.8.1-24 和 g++ 的模拟行为。此外, 声称 MSVC 还将 inline 关键字作为实际内联请求的标题。

g++、clang++/LLVM 和 MSVC 共同涵盖了 C++ 的很大一部分 "market",因此可以肯定地说,编译器几乎普遍将内联视为内联请求,无论他们是否注意到,并且除了 C++ 标准的其他要求。

标记它inline

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

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 语义(即使对于 static 成员函数)。

如果不实际声明(显式或隐式)inline,也无法为任何函数请求 inline 语义。换句话说,没有办法说 "make this function behave like inline in everything except actual inlining".

另一方面,函数 templates 的语义与 inline 函数的语义相似,无需向编译器请求内联(尽管现在毫无意义)他们在他们的呼叫站点。

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