【问题标题】:Why can't I initialize an array of objects if they have private copy constructors?如果对象数组具有私有复制构造函数,为什么我不能初始化它们?
【发布时间】:2013-01-10 16:48:53
【问题描述】:

我刚刚在处理 C++ 项目时遇到了一些意外且令人沮丧的行为。我的实际代码稍微复杂一些,但下面的示例也很好地捕捉到了它:

class Irritating
{
    public:  Irritating() {}
    private: Irritating(const Irritating& other) {}
};

const Irritating singleton;                // Works just fine.
const Irritating array[] = {Irritating()}; // Compilation error.

int main()
{
    return 0;
}

尝试编译它会产生以下错误(以防万一抛出 GCC 版本):

[holt@Michaela irritating]$ g++ --version
g++ (GCC) 4.6.3 20120306 (Red Hat 4.6.3-2)
Copyright (C) 2011 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

[holt@Michaela irritating]$ g++ test.cpp
test.cpp:4:11: error: ‘Irritating::Irritating(const Irritating&)’ is private
test.cpp:8:41: error: within this context
[holt@Michaela irritating]$ 

不幸的是,有问题的对象来自外部库,不受我的控制。我目前的解决方法是使用指针数组;它有效,但感觉有点hackish,并增加了不必要的间接层。有没有更好的方法来做到这一点?

另外:数组是常量和全局的(在实际代码中是类静态的);为什么它没有被初始化到位?这是预期的 C++ 行为,还是 GCC 的错误/怪癖?

更新:安装 Clang 只是为了看看它是否同意 GCC。可悲的是,它做到了:

[holt@Michaela irritating]$ clang test.cpp
test.cpp:8:29: warning: C++98 requires an accessible copy constructor for class 'Irritating' when binding a reference to a temporary; was private
      [-Wbind-to-temporary-copy]
const Irritating array[] = {Irritating()};
                            ^
test.cpp:4:11: note: declared private here
        private: Irritating(const Irritating& other) {}
                 ^
test.cpp:8:29: error: calling a private constructor of class 'Irritating'
const Irritating array[] = {Irritating()};
                            ^
test.cpp:4:11: note: declared private here
        private: Irritating(const Irritating& other) {}
                 ^
1 warning and 1 error generated.
[holt@Michaela irritating]$

【问题讨论】:

  • 这就是指定语言的方式。从花括号列表初始化是一个正式的副本,并且需要可以访问复制构造函数(即使它实际上从未被调用过)。
  • @KerrekSB: even though in practice it may never be called*
  • @Non-StopTimeTravel:嘿,你又是别人吗?!停下来!!! :-)
  • @KerrekSB:冬天我没有得到足够的帽子,所以我自己做了一些,我正在连续试戴

标签: c++ g++ initialization copy-constructor


【解决方案1】:

尝试创建一个大小的数组?应该调用默认的ctor。

就像这个source

struct foo {
   int x;
   foo():x(1) {}
private:
   foo( foo const& ) {}
};

foo array[10];

#include <iostream>
int main() {
   for (auto&& i:array) {
      std::cout << i.x << "\n";
   }
}

它演示了在没有默认复制构造函数的数组中初始化foo

如果您的问题是您实际上想使用非默认构造函数构造 foo,也可以这样做,但要困难得多,这不是您的问题。无论如何,这里有一个非常非常粗略的草图,描述了创建支持其元素的就位构造的类似数组的结构所需的东西。远未完成或编译,但基本技术应该是健全的:

#include <cstddef>
#include <utility>
#include <type_traits>


template<typename... Args>
struct types {};

template<typename types, typename=void>
struct emplacer;

template<typename T>
struct remove_refref {
  typedef T type;
};
template<typename T>
struct remove_refref<T&&> {
  typedef T type; 
};

template<typename A1, typename... Args>
struct emplacer< types<A1, Args...>, void>:
  emplacer< types<Args...> >
{
  typename remove_refref<A1>::type val;
  emplacer( A1 arg, Args... args ):
    emplacer< types<Args...>, index+1 >( std::forward(args)... ),
    val( std::forward(arg) )
  {}
};

template< std::size_t n >
struct extract {
  template< typename A1, typename... Args >
  A1&& from( emplacer<types<A1, Args...>&& e ) {
    return extract<n-1>::from( emplacer<types<Args...>>&&(e) );
  }
};
template<>
struct extract<0> {
  template< typename A1, typename... Args >
  A1&& from( emplacer<types<A1, Args...>&& e ) {
    return std::move( e.val );
  }
};

template<std::size_t... v>
struct seq {};
template<std::size_t n, std::size_t... tail>
struct make_seq: make_seq<n-1, n-1, tail...> {};
template<std::size_t n, std::size_t... tail>
struct make_seq<0, tail...> {
  typedef seq<tail...> type;
  type val() { return type(); }
};
struct nothing {};
template<typename T, typename... Args, std::size_t... indexes>
nothing construct( T* src, emplacer<types<Args...>>&& e, seq<indexes...> s = make_seq< sizeof...(Args) >::val() ) {
  new(src)T( std::move( extract<indexes>( std::move(e) ))... );
}

template<typename... Args>
emplacer< types<Args...> > emplace( Args&&... a ) {
  return emplacer< types<Args...> >( std::forward(a)... );
}

template<typename T, std::size_t n>
struct my_array {
private:
  union T_mem {
    T t;
    char x;
    T_mem():x(0) {}
  };
  T_mem buff[n];
  template<typename... nothings>
  void do_nothing( nothings...&& ) {}
  template<typename... emplacers, std::size_t... indexes>
  my_array( emplacers&&... em, seq<indexes...> s=make_seq< sizeof...(emplacers) >::val() ) {
    do_nothing( construct( &buff[indexes].t, em)... );
  }
  ~my_array() {
    for( auto&& v:buff) {
      v.t.~T();
    }
  }
  T& operator[](std::size_t n) { return buff[n].t; }
  // etc
};

我们的想法是,我们创建一个类似数组的构造,它实际上是unionTchar 的数组。因此,我们避免实际构建我们的T,同时仍然具有适当的对齐方式等。假设char 没有非平凡的对齐,则生成的缓冲区与T[] 二进制兼容。

然后我们将emplacer 对象作为构造参数,它们充当任意构造参数的包。出于恼人的原因,这些对象需要为其某些参数创建一个临时副本(我不知道如何避免生命周期问题……也许我遗漏了一些东西)。

my_array 的构造函数接受任意数量的emplacers 并根据它们的参数继续构造buff 的内容。

你会像这样创建你的数组:

my_array< Foo, 10 > arr = {
  emplacer( a, b, c ),
  emplacer( x, y, z ),
  ...
};

更多的工作将允许在数组中默认构造未初始化的对象。

但这真的很难写好。

【讨论】:

  • @XavierHolt 您是否包含{}?不要那样做。如果您不尝试创建副本,将调用默认构造函数。
【解决方案2】:

假设 Irritating 的复制构造函数被禁用,因为它比通过引用管理它们的成本更高:

vector<unique_ptr<Irritating>> V = { new Irritating(), ... };

您可以使用shared_ptr 代替unique_ptr,具体取决于使用模式。

(如果你可以修改 Irrating 你可以给它一个移动构造函数,看看移动语义)

如果您真的希望它们就地构建,那么您可以使用aligned_storage 为它们创建一个存储阵列,然后将它们放置在适当的位置。这将生成与您想要对原始请求执行的操作几乎相同的编译代码,但它有点混乱:

aligned_storage <sizeof(Irritating), alignment_of<Irritating>::value>::type data[N];
new ((Irritating*) data+0) Irritating(...);
new ((Irritating*) data+1) Irritating(...);
new ((Irritating*) data+2) Irritating(...);
...
new ((Irritating*) data+N-1) Irritating(...);

(不要忘记在程序退出时放置删除它们。)

【讨论】:

  • +1 正是我想到的内存布局。遗憾的是它使构造/破坏变得多么混乱,但我应该能够将其封装在一个单独的文件中的某个地方并保持它不碍事......我一定会试试这个 - 谢谢!
【解决方案3】:
class Irritating
{
    public:  Irritating() {}
    private: Irritating(const Irritating& other) {}
};

enum DefaultConstruct { defaultConstruct };

class MaybeTooClever
    : public Irritating
{
public:
    MaybeTooClever( DefaultConstruct = defaultConstruct ) {}
#ifdef __GNUC__
public:
    MaybeTooClever( MaybeTooClever const& other );      // No such.
#else
private:
    MaybeTooClever( MaybeTooClever const& other );      // No such.
#endif
};    

static MaybeTooClever const array[] = { defaultConstruct };

int main()
{}

【讨论】:

  • 也许只是删除预处理器条件并让复制构造函数声明为public。然后它将是符合标准的,只会导致任何编译器愚蠢到可以实际复制的链接错误(但我不相信任何编译器会这样做)。我将条件放入检查 Visual C++ 是否在没有可访问的复制构造函数的情况下编译它,它确实如此,而 g++ 没有。
  • 工作得很好。因为在我的实际代码中,问题对象是我的一个类的成员,所以一个虚拟复制构造函数修复了所有问题。我只是希望它不像偷偷摸摸的标准!
【解决方案4】:

因为单个数组元素是由 copy-initialization 从通过 = {...} 语法指定的初始化程序初始化的。见 8.5/12 (C++03)

在参数传递、函数返回时发生的初始化, 抛出异常 (15.1)、处理异常 (15.3) 和 大括号括起来的初始化列表(8.5.1)称为复制初始化

复制初始化需要复制构造函数(即使它不会实际使用它)。

实际上,如果您通过公开复制构造函数来编译代码,编译器最终可能会在不使用复制构造函数的情况下初始化您的数组元素。然而,抽象语言的正式规则要求在这种情况下进行复制初始化。

【讨论】:

  • 哼。那么指针就是了。还是谢谢!
  • @XavierHolt 那么移动构造函数呢?
  • @SethCarnegie - 那很好,但没有这样的运气。它来自尚未使用 C++11 的外部库。
猜你喜欢
  • 2011-02-11
  • 2016-11-28
  • 2011-02-04
  • 1970-01-01
  • 2019-08-28
  • 2015-03-09
  • 2012-08-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多