【问题标题】:Is pimpl compatible with anonymous namespaces?pimpl 与匿名命名空间兼容吗?
【发布时间】:2011-04-21 14:38:49
【问题描述】:

我正在尝试使用 pimpl 模式并在匿名命名空间中定义实现类。这在 C++ 中可能吗?我的失败尝试如下所述。

是否可以在不将实现移动到具有名称(或全局名称)的名称空间中的情况下解决此问题?

class MyCalculatorImplementation;

class MyCalculator
{
public:
    MyCalculator();
    int CalculateStuff(int);

private:
    MyCalculatorImplementation* pimpl;
};

namespace // If i omit the namespace, everything is OK
{
    class MyCalculatorImplementation
    {
    public:
        int Calculate(int input)
        {
            // Insert some complicated calculation here
        }

    private:
        int state[100];
    };
}

// error C2872: 'MyCalculatorImplementation' : ambiguous symbol
MyCalculator::MyCalculator(): pimpl(new MyCalculatorImplementation)
{
}

int MyCalculator::CalculateStuff(int x)
{
    return pimpl->Calculate(x);
}

【问题讨论】:

    标签: c++ namespaces pimpl-idiom


    【解决方案1】:

    不,在使用指针类型之前必须至少声明类型,并且将匿名命名空间放在标头中实际上是行不通的。但你为什么要这样做呢?如果您真的想隐藏实现类,请将其设为私有内部类,即

    // .hpp
    struct Foo {
        Foo();
        // ...
    private:
        struct FooImpl;
        boost::scoped_ptr<FooImpl> pimpl;
    };
    
    // .cpp
    struct Foo::FooImpl {
        FooImpl();
        // ...
    };
    
    Foo::Foo() : pimpl(new FooImpl) { }
    

    【讨论】:

    • 这也是我使用时间最长的,直到有人向我指出,如果你导出类Foo,它也会导出类Foo::FooImpl,这通常不是你想要的...
    • @mmutz export 是指 MS 相关的__declspec(dllexport) 吗?如果是,我可能不需要担心。
    • @anatolyg:是的,或者在 GCC/ELF 系统上是 __attribute__((visibility=default))
    【解决方案2】:

    是的。有一个解决方法。将头文件中的指针声明为 void*,然后在实现文件中使用重新解释转换。

    注意:这是否是一个理想的解决方法完全是另一个问题。正如人们常说的,我将把它留给读者作为练习。

    请参阅下面的示例实现:

    class MyCalculator 
    {
    public:
        MyCalculator();
        int CalculateStuff(int);
    
    private:
        void* pimpl;
    };
    
    namespace // If i omit the namespace, everything is OK
    {
        class MyCalculatorImplementation
        {
        public:
            int Calculate(int input)
            {
                // Insert some complicated calculation here
            }
    
        private:
            int state[100];
        };
    }
    
    MyCalculator::MyCalculator(): pimpl(new MyCalculatorImplementation)
    {
    }
    
    MyCalaculator::~MyCalaculator() 
    {
        // don't forget to cast back for destruction!
        delete reinterpret_cast<MyCalculatorImplementation*>(pimpl);
    }
    
    int MyCalculator::CalculateStuff(int x)
    {
        return reinterpret_cast<MyCalculatorImplementation*>(pimpl)->Calculate(x);
    }
    

    【讨论】:

    • 如果reinterpret_cast 真的有必要,我认为经典的 pimpl 不会那么受欢迎。这显然不是要走的路……
    • @m8mble 明确地说,提出的问题不是它是否可取;问题是它是否可能。如上所述,尽管其他答案相反,这绝对是可能的。
    • @m8mbl 您关于是否有必要的问题完全是另一个问题。所以这里的反对票似乎有点多。该帖子仍然提供丰富的信息,并提供了可能对某人有用的解决方法。
    【解决方案3】:

    不,你不能那样做。您必须前向声明 Pimpl 类:

    class MyCalculatorImplementation;
    

    并且声明了类。如果然后将定义放入未命名的命名空间中,则您正在创建另一个类(anonymous namespace)::MyCalculatorImplementation,它与::MyCalculatorImplementation 无关。

    如果这是任何其他命名空间 NS,您可以修改前向声明以包含命名空间:

    namespace NS {
        class MyCalculatorImplementation;
    }
    

    但是未命名的命名空间,尽管它很神奇,但当该标头包含在其他翻译单元中时,它会解析为其他内容(每当您将该标头包含到另一个翻译单元中时,您将声明一个新类)。

    但是这里不需要使用匿名命名空间:类声明可能是公共的,但定义在实现文件中,仅对实现文件中的代码可见。

    【讨论】:

      【解决方案4】:

      如果你真的想在你的头文件中使用前向声明的类名并在模块文件中的匿名命名空间中实现,那么将声明的类设为接口:

      // header
      class MyCalculatorInterface;
      
      class MyCalculator{
         ...
         MyCalculatorInterface* pimpl;
      };
      
      
      
      //module
      class MyCalculatorInterface{
      public:
          virtual int Calculate(int) = 0;
      };
      
      int MyCalculator::CalculateStuff(int x)
      {
          return pimpl->Calculate(x);
      }
      
      namespace {
          class MyCalculatorImplementation: public MyCalculatorInterface {
              ...
          };
      }
      
      // Only the ctor needs to know about MyCalculatorImplementation
      // in order to make a new one.
      MyCalculator::MyCalculator(): pimpl(new MyCalculatorImplementation)
      {
      }
      

      【讨论】:

      • 你仍然会用 Interface 类污染公共类的命名空间 -> 没有收获。
      【解决方案5】:

      markshiz 和 quamrana 为以下解决方案提供了灵感。

      class Implementation,旨在在全局头文件中声明,并用作代码库中任何 pimpl 应用程序的void*。它不在匿名/未命名的命名空间中,但由于它只有一个析构函数,因此命名空间污染仍然可以接受。

      class MyCalculatorImplementation 派生自 class Implementation。因为pimpl 被声明为std::unique_ptr&lt;Implementation&gt;,所以不需要在任何头文件中提及MyCalculatorImplementation。所以现在MyCalculatorImplementation 可以在匿名/未命名的命名空间中实现。

      好处是MyCalculatorImplementation 中的所有成员定义都在匿名/未命名的命名空间中。您必须付出的代价是您必须将Implementation 转换为MyCalculatorImplementation。为此,提供了转换函数toImpl()。 我怀疑是使用dynamic_cast 还是static_cast 进行转换。我猜dynamic_cast 是典型的规定解决方案;但static_cast 也可以在这里工作,并且可能性能更高。

      #include <memory>
      
      class Implementation
      {
      public:
        virtual ~Implementation() = 0;
      };
      inline Implementation::~Implementation() = default;
      
      class MyCalculator
      {
      public:
        MyCalculator();
        int CalculateStuff(int);
      
      private:
        std::unique_ptr<Implementation> pimpl;
      };
      
      namespace // Anonymous
      {
      class MyCalculatorImplementation
        : public Implementation
      {
      public:
        int Calculate(int input)
        {
          // Insert some complicated calculation here
        }
      
      private:
        int state[100];
      };
      
      MyCalculatorImplementation& toImpl(Implementation& impl)
      {
        return dynamic_cast<MyCalculatorImplementation&>(impl);
      }
      }
      
      // no error C2872 anymore
      MyCalculator::MyCalculator() : pimpl(std::make_unique<MyCalculatorImplementation>() )
      {
      }
      
      int MyCalculator::CalculateStuff(int x)
      {
        return toImpl(*pimpl).Calculate(x);
      }
      

      【讨论】:

        猜你喜欢
        • 2017-02-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-14
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多