【问题标题】:Hiding members in public interface and ODR在公共界面和 ODR 中隐藏成员
【发布时间】:2015-03-31 16:39:22
【问题描述】:

我在一个库中有多个类,这些类具有我希望对客户端代码隐藏的内部结构。 从客户端的角度来看,每个类都是从一个库类中查询的,并且仅用作不透明的指针.一个例子如下:

struct SomeSystem;
void doSomethingToSomeSystem(SomeSystem* system, Parameters params);
void doSomethingElseToSomeSystem(SomeSystem* system, Parameters params);

在实现方面,SomeSystem 有多个调用者不可见的成员。这一切都很好,但我不太喜欢笨拙的使用语法:

SomeSystem* system = lib->getSomeSystem();
doSomethingToSomeSystem(system, params);
doSomethingElseToSomeSystem(system, params);

另一种方法是这样的:

struct SomeSystem;
namespace somesystem {
    void doSomething(SomeSystem* system, Parameters params);
    void doSomethingElse(SomeSystem* system, Parameters params);
}

附使用代码:

SomeSystem* system = lib->getSomeSystem();
somesystem::doSomething(system, params);
somesystem::doSomethingElse(system, params);

我还可以使用名为doSomethingdoSomethingElse 的全局方法,如果另一种类型也定义了doSomething,则依赖于函数重载。但是,在这种情况下,很难在 IDE 中找到 SomeSystem 的所有“成员”。

我很想实际使用成员函数:

struct SomeSystem {
    void doSomething(Parameters params);
    void doSomethingElse(Parameters params);
};

附使用代码:

SomeSystem* system = lib->getSomeSystem();
system->doSomething(params);
system->doSomethingElse(params);

最后一个 sn-p 对我来说看起来不错,但 SomeSystem 不再是一个不透明的指针 - 它实际上定义了成员。我对此有点警惕。一个潜在的问题是单一定义规则。但是,类的“公共”定义和“私有”定义将只对不同的翻译单元可见。这里是否还隐藏着其他不好的东西?如果客户端代码尝试在堆栈上实例化 SomeSystem 或使用 new 它显然会使程序崩溃。但我愿意接受。也许我可以通过在公共接口中提供一个私有构造函数来解决这个问题。

另一种方法当然是用纯虚方法定义一个抽象类。 但是,如果不是绝对必要,我想避免这种开销。

编辑:

需要明确的是,我想知道客户端包含的公共标头包含与实现使用的类的不同定义(缺少一些成员)是否合法,因为客户端从不实例化类。

公共标头:

struct SomeSystem {
    void doSomething(Parameters params);
    void doSomethingElse(Parameters params);
};

私人标头:

struct SomeSystem {
    Member member;
    void doSomething(Parameters params);
    void doSomethingElse(Parameters params);
};

私有源(包括私有标头):

void SomeSystem::doSomething(Parameters params) {
    ...
}
void SomeSystem::doSomethingElse(Parameters params) {
    ...
}

这在我测试时有效,但我不确定它是否以某种方式违反标准。这两个标题永远不会包含在同一个翻译单元中。

【问题讨论】:

  • 是的,我很熟悉它,我宁愿避免它。它引入了额外的内存分配并使实现复杂化。
  • 请注意,我特别喜欢语法糖,但不会使我的解决方案性能或复杂性更差。
  • 您可以让类拥有接口,而不是使用接口超类/父级。这样,接口保持静态,您只需创建一个类来拥有该接口并在需要时使用它。
  • 这需要虚函数,我想避免。

标签: c++ c++11 static-libraries one-definition-rule


【解决方案1】:

在这种情况下,PIMPL 惯用语可能是理想的,但它是每次访问的额外间接,所以就是这样。

如果您只是在使用一些语法糖,另一种选择可能是利用 ADL - 它至少可以将系统名称排除在函数名称之外:

// publicly shared header file
namespace one_system
{
  struct system;
  typedef system* system_handle;
  void do_something(system_handle );
};

// private implementation
namespace one_system
{
  struct system {};
  void do_something( system_handle ) { cout << "one"; }
};


int main() {
  auto handle = /* SOMETHING TO GET THIS SYSTEM */;
  do_something(handle); //do_something found by ADL
  return 0;
}

编辑

我仍然认为 PIMPL 是理想的。与已有的相比,您也不一定需要分配或任何额外的开销。

如果您有一个 system* 和一个函数声明(根据您的示例),编译器已经必须执行间接操作。您需要跳转到该函数(因为它在另一个翻译单元中定义)和间接访问函数内的系统(因为它被视为指针)。

您真正需要做的就是为类定义一个接口,如下所示:

// put this in a namespace or name it according to the system
class interface
{
    system_handle m_system;

    public:
    interface( system_handle s ) : m_system( s ) {}
    interface() = delete;

    void do_something();
};

现在在另一个翻译单元中,do_something() 被定义为在系统上执行它的操作。 lib->GetSystem() 可以返回接口的一个实例。该接口可以在头文件中完全声明,因为它只包含公共函数。系统仍然是完全私有的(因为 lib 的用户不会有头文件来声明它的内容)。

此外,用户可以轻松地复制界面。它不关心它的指针来自哪里,因此库可以根据需要传入静态地址。

我可以看到的一个缺点是成员变量需要有访问器(并且有很多人会争辩说每个成员变量都应该是私有的,并且无论如何都有公共或受保护的访问器)。

另一个是接口的 *this 将被传递到 do_something 并且可能不需要。这也可以通过在头文件中定义 do_something 来解决:

void do_something() { do_something_to_system( m_system ); }

现在编译器应该能够优化 *this 输出,因为 do_something 可以内联,并且编译器可以轻松地插入代码以将 m_system 加载到寄存器中并调用 do_something_to_system (这与您在你的例子)。

【讨论】:

  • 我不同意。 PIMPL 并不理想,因为就像您说的那样,它引入了额外的间接性。更糟糕的是,它引入了额外的内存分配并给实现带来了更多负担。除非您执行 using namespace do_something,否则您的 one_system 示例将不起作用。这也正是我在我的问题中所讨论的。不过感谢您的回答。
  • @rasmus 它的哪一部分不起作用?您不需要使用命名空间 do_something 来让编译器找到函数 - 您只需要将句柄或系统声明在与函数相同的命名空间中。
  • 好吧,我错过了在命名空间内定义的系统。我仍然认为除非您键入 one_system:: 否则 IDE 不会列出成员是一个问题。
  • 所以您的答案基本上是上述替代方案的变体。感谢您在列表中添加另一个建议。但是,我仍然认为最后一个版本是最好的。我想知道的是该版本是否损坏,如果是,原因是什么。它在多个编译器和平台上非常有效。
  • @rasmus 您提到的最后一个版本没有任何问题,但我认为您的查询是:如何以低性能开销保持预期的语法,但让系统的内部结构对图书馆。我已经修改了我的答案,增加了一个选项供考虑。
猜你喜欢
  • 2015-06-20
  • 2011-01-12
  • 1970-01-01
  • 2020-06-16
  • 2017-12-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多