一个类可以是一个翻译单元的私有成员吗?

7

Consider the following code:

/*
 * myclass.h
 */

class myclass_impl
{
    // ...
}

boost::shared_ptr<myclass_impl> myclass;

我能否某种方式使myclass_impl(或者至少是它的直接使用)对定义它的翻译单元私有化,从而允许客户端只使用myclass类型定义?我试图实现的目标是让编译器在某人直接使用实现类时警告我。

2
简短回答:不行(客户端总是可以使用 myclass::value_type)。长回答:您所说的“使用”是什么意思?通过使某些构造函数和析构函数不可用,可以禁止某些用法。 - Kerrek SB
我的意思不仅是实例化,还包括对类的任何引用(例如在函数中将其用作参数类型)。我担心在这种情况下无法将其设置为私有:( - Dan Nestor
@dandrestor,通过将整个类型设为私有(放入某个类中),可以使引用类无效 :) - ony
5个回答

3

在源文件中声明你的类(而非头文件),这样其他编译单元就无法访问它。然后,在头文件中使用前向声明来声明一个指针/引用。

或者定义一个实现头文件,并注释说明它不应该被包含在其他源文件中。

/*
 * myclass.h
 */

class myclass_impl;

class myclass
{
  boost::shared_ptr<myclass_impl> myclass_i;
public:
  myclass() : myclass_i(new myclass_impl) { }
  int getI() const;
};


/*
 * myclass.cpp
 */

class myclass_impl
{
  int i;
public:
  myclass_impl() : i(4) { }
  int getI() const { return i; }  
};

int myclass::getI() const 
{ 
  return myclass_i->getI(); 
}

请您详细说明一下?如果在头文件中使用前向声明,那么该类不会对包含该头文件的所有其他翻译单元可用吗? - Dan Nestor
是的,它会。但是他们只会知道有一个名为myclass_impl的类,而不知道它的样子(因为你将myclass_impl声明隐藏在myclass.cpp文件中)。 - Werner Henze
这意味着在客户端代码中使用对myclass_impl的引用或指针不会导致编译器错误,这正是我要实现的目标。我的理解正确吗? - Dan Nestor
是的,但是他们会如何处理指针或引用呢?因为你无法对其进行解引用。 - stefaanv
这是您关注的答案:我想要实现的是,如果有人直接使用实现类,则编译器会向我发出警报。 - stefaanv
@dandrestor,你可以选择使用 void * 类型,但我认为这是一个不太好的方向。更好的方法是使用一些完全抽象的类,只包含虚析构函数。 - ony

3

您可以创建接口myclass并使用工厂方法提供在同一文件中定义的匿名命名空间中的私有类myclass_impl的实例。另一种变体是Pimpl。

文件myclass.h

class myclass
{
public:
    virtual void doSomething() = 0;
    static boost::shared_ptr<myclass> createInstance();
};

文件 myclass_impl.cpp

#include "myclass.h"
namespace {
    class myclass_impl : public myclass
    {
    public:
        virtual void doSomething() { std::cerr << "Hi there" << std::endl; }
    };
}
static boost::shared_ptr<myclass> myclass::createInstance()
{
    return new myclass_impl();
}

更新(新增可怕的解决方案):

myclass.h

class destroyable { virtual ~destroyable() {} };
class myclass {
private:
    boost::shared_ptr<destroyable> pimpl;
public:
    void doSomething();
};

myclass_impl.cpp

namespace {
    class myclass_impl : public destroyable {
    public:
        void doSomething() { /* ... */ }
    };
}
void myclass::doSomething() { static_pointer_cast<myclass>(pimpl)->doSomething(); }

好的,在我的实现中,myclass_impl 有私有构造函数,并且只能由一个单独的友元工厂类创建。然而,我需要使工厂类公开可用。您能详细说明一下您的答案吗? - Dan Nestor
你的示例是否可以进行改编,以便我不需要使用createInstance()?我希望通过更简单的语法myclass_object->method()来访问myclass_impl的成员。 - Dan Nestor
1
无论是通过工厂对象创建还是直接通过静态工厂方法创建,它们之间没有任何区别。私有构造函数只会对使用 myclass f()void f(myclass x) 的地方产生影响,我想。但你仍然能够使用 void f(myclass &x)myclass *f()。实际上,我认为你需要隐藏实现细节(即避免在头文件中显示 private 部分)。看起来我不太理解你想要实现的目标。你需要什么样的细节呢? - ony
你需要暴露出一个可以创建对象的东西,或者提供一个现有实例。否则,你就无法访问myclass_impl。你可以使用“myclass_object = myclass::createInstance(); myclass_object->doSomething();”。 - ony
谢谢您的评论,顺便说一句。它最终让我彻底理解了pimpl的含义 :) - Dan Nestor
显示剩余5条评论

1

免责声明,本答案中使用的publicprivate是泛指,并不按照标准中的定义,而是表示其他翻译单元可以访问使用的意思。根据标准,private用于表示公共成员

这完全取决于类的具体定义以及需要向其他翻译单元发布多少内容。

如果你只在.cpp文件中声明和定义该类(我还会使用一个未命名的命名空间来避免名称冲突),那么该类型将无法从该翻译单元之外访问。

如果在任何地方引用了该类,并且必须将其发布到该翻译单元之外(存在一个公共类型的成员,该成员是指向公共类的指针/引用),那么最好的方法就是在头文件中提供一个前向声明(现在不再是未命名的命名空间,可能作为使用它的类的private类型内部)。

作为最后的手段,您可以在头文件中提供类型的整个定义(公共类型直接持有该类型的成员),但仍然可以将该类型作为私有内部类型保留在公共类型中,禁止在该类型之外使用它(包括友元)。

正如您所看到的,私有和公共的含义以及可以控制的内容是不同的。通过在头文件中不提供定义并使用未命名的命名空间,您使其对其他TU不可访问;通过仅提供前向声明,该类型已知存在,但不能在需要完整类型的任何上下文中使用(函数仍然可以接受和转发指向该类型的指针)。在另一个层面上,通过将其设置为内部类型并将其设置为不同类型的private,定义将被知道,但它将无法在封闭类型的friend之外使用...


谢谢。我想我需要的东西最接近的是在头文件中进行前向声明,然后在 .cpp 文件中完全声明和定义该类。 - Dan Nestor

1

不清楚你想要实现什么。但是已经有人问过你了,而你并没有很好地澄清。在你的回答评论中,你写道:“我的意思不仅仅是禁止实例化,还包括任何对该类的引用(例如在函数中使用它作为参数类型)”。

从字面上理解,这意味着在你的头文件中使用以下内容:

struct BlahImpl;
typedef boost::shared_ptr<BlahImpl> Blah;

// Functions that create or give access to Blah instances.

客户端代码可以创建或访问Blah实例,并复制它们(具有隐含的共享语义),但实际上不能对它们执行任何操作。最好的情况是,它们可以作为某个函数之前已调用(生成实例)的证明。或者也许某些东西由涉及这些实例的函数调用模式控制,但无论如何,boost::shared_ptr将完全无关紧要和多余。

所以也许你并不是确切地意思是你写的那样,而是更像是“任何BlahImpl实例都应该是动态分配的,并由boost::shared_ptr封装”。

如果是这样,你可以按照以下方式实现:

  • 您可以通过使析构函数非public(最好是protected),并提供某些销毁实例的方法(最简单的方法是将friend授予一个常见的销毁函数模板),来强制执行动态分配。

  • 您可以通过多种方式确保给定智能指针的包装。主要问题是转发构造函数参数。一种与C++98兼容的方法是通过宏进行转发,在这种情况下,“您不能无意中创建除此宏之外的实例”可以通过混淆来实现,即混淆new表达式。

示例:

#include <boost/shared_ptr.hpp>
#include <iostream>
#include <stddef.h>         // ptrdiff_t, size_t
#include <string>
using namespace std;

namespace cpp11 {
    using boost::shared_ptr;
};

template< class Type >
void destroy( Type const* p ) { delete p; }

class OnlySharedPtrUsage
{
public:
    virtual ~OnlySharedPtrUsage() {}

    struct InstantiationObfuscation;

    static void* operator new( size_t size, InstantiationObfuscation* )
    {
        return ::operator new( size );
    }

    static void operator delete( void* p, InstantiationObfuscation* )
    {
        ::operator delete( p );
    }

    static void operator delete( void* p )
    {
        ::operator delete( p );
    }
};

#define NEW_SHARED( type, args )                                \
    ::cpp11::shared_ptr<type>(                                  \
        new( (type::InstantiationObfuscation*)0 ) type args,    \
        destroy<type>                                           \
        )

class MyClass
    : public OnlySharedPtrUsage     // The NEW_SHARED macro simplies.
{
template< class Type > friend void destroy( Type const* );
private:
    string  helloText_;

    MyClass( MyClass const& );                      // No such.
    MyClass& operator=( MyClass const& );           // No such.

protected:
    virtual ~MyClass()              // Only dynamic allocation allowed.
    {
        cout << "MyClass::<destroy>()" << endl;
    }

public:
    string helloText() const { return helloText_; }

    MyClass( string const& text )
        : helloText_( text )
    {
        cout << "MyClass::<init>( string )" << endl;
    }
};

int main()
{
    // MyClass     o( "a" );                   // ! Does not compile, not dynamic.
    // MyClass*     p  = new MyClass( "b" );   // ! Does not compile, not "mangled".
    cpp11::shared_ptr< MyClass > sp  = NEW_SHARED( MyClass,( "Hello from MyClass!" ) );

    cout << sp->helloText() << endl;
}

请注意,此示例不直接支持make_shared的优化。混淆的分配器函数(正式为放置函数)与make_shared不太匹配。但我想可以通过定义分配器类并使用alloc_shared来完成。
另请注意,此方法支持仅头文件的模块;无需单独编译。 :-)
哦,对于一般情况,您还需要添加数组的分配器函数。

1

嗯...

class myclass
{
private:
    class myclass_impl
    {
    public:
        void do_something() { std::cerr << "Hello there" << std::endl; }
    };

public:

    typedef boost::shared_ptr<myclass_impl> ptr_type;

    static ptr_type construct()
    { return ptr_type(new myclass_impl()); }
};

int main()
{    
    myclass::myclass_impl *x; // error: 'class myclass::myclass_impl' is private
    myclass::ptr_type::element_type *y; // ok
    myclass::ptr_type x = myclass::construct();
    x->do_something(); /// Hello there
}

这是你想要的吗?

顺便提一下,无法隐藏 myclass_impl,因为 boost::shared_ptr<T> 提供了访问底层类型的方式。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接