RVO/NRVO и общедоступный неопределенный конструктор копирования

Я сейчас борюсь со следующим предложением и хочу знать юридические и, в меньшей степени, моральные аргументы против него или за него.

Что у нас было:

#include <vector>

class T;

class C
{
public:
    C() { }
    ~C( ) { /*something non-trivial: say, calls delete for all elements in v*/ }
    // a lot of member functions that modify C
    // a lot of member functions that don't modify C
private:
    C(C const &);
    C& operator=(C const&);
private:
    std::vector< T* > v;
};

void init(C& c) { } // cannot be moved inside C

// ...
int main()
{
    // bad: two-phase initialization exposed to the clients
    C c;
    init(c);

    // bad: here follows a lot of code that only wants read-only access to c
    //      but c cannot be declared const
}

Что было предложено:

#include <vector>

class T;

class C
{
public:
    C() { }
    ~C( ) { /*calls delete for all elements in v*/ }

    // MADE PUBLIC
    C(C const &); // <-- NOT DEFINED

    // a lot of member functions that modify C
    // a lot of member functions that don't modify C
private:
    C& operator=(C const&);
private:
    vector< T* > v;
};

C init() // for whatever reason object CANNOT be allocated in free memory
{
    C c;
    // init c
    return c;
}

// ...
int main()
{
    C const & c = init();
}

Это компилирует и связывает (и работает) с использованием последнего g++ (который является единственным целевым компилятором) как 4.1.2, так и 4.4.5 - из-за (N) RVO конструктор копирования никогда не вызывается; деструктор вызывается только в конце main().

Утверждается, что этот метод совершенно прекрасен, потому что невозможно неправильно использовать конструктор-копию (если он когда-либо был сгенерирован, это было бы ошибкой компоновщика), а его общедоступность предотвращает жалобы компилятора на частный.

Мне кажется очень-очень неправильным использовать такой трюк, который, как мне кажется, противоречит духу C++ и больше похож на хак - в плохом смысле этого слова.

Мои ощущения не являются достаточной аргументацией, поэтому я сейчас ищу технические детали.

Пожалуйста, не размещайте здесь учебники C++:

  • Я знаю о «Правиле трех» и прочитал 12.8/15 и 12.2 Holy Standard;
  • Я не могу использовать ни vector<shared_ptr<T> >, ни ptr_vector<T>;
  • Не могу выделить C в свободной памяти и вернуть из init через C*.

Спасибо.


person Alexander Poluektov    schedule 05.04.2011    source источник
comment
Я предполагаю, что строительство переезда для вас невозможно?   -  person Konrad Rudolph    schedule 05.04.2011
comment
@Konrad Rudolph: к сожалению, нет (g ++ 4.1.2 - один из компиляторов, которые мы должны поддерживать, поэтому нет функций 0x).   -  person Alexander Poluektov    schedule 05.04.2011
comment
сделать его общедоступным не позволяет глупому компилятору жаловаться на частный. - не тупой компилятор. Стандарт требует, чтобы конструктор копирования был доступен, даже если копия исключена, потому что исключение копии необязательно. Таким образом, если бы ей было разрешено быть приватной, то программа, которая полагается на исключение, чтобы избежать ошибки, в любом случае не будет строго соответствовать, и в этом случае стандарт требует, чтобы это было диагностировано. Компилятор помогает вам писать переносимый код, по общему признанию, вопреки вашему желанию ;-)   -  person Steve Jessop    schedule 05.04.2011
comment
@Стив Джессоп: я знаю. Извините, это была саркастическая часть заявленного. Я удалил это из текста, чтобы люди не обращали внимания на несущественные детали.   -  person Alexander Poluektov    schedule 05.04.2011
comment
Другая альтернатива: init может принимать (пустой) boost::Optional‹C› в качестве параметра by-ref, а затем создавать объект в этом необязательном, возвращая константную ссылку на объект внутри boost::Optional. В этом случае ctor по умолчанию также будет закрытым, а init будет сделан friend типа C. -- Просто идея.   -  person Martin Ba    schedule 05.04.2011


Ответы (3)


Это компилирует и связывает (и работает) с использованием последнего g++ (который является единственным целевым компилятором) как 4.1.2, так и 4.4.5 - из-за (N) RVO конструктор копирования никогда не вызывается; деструктор вызывается только в конце main().

Хотя это может работать с GCC, ваш код действительно имеет неопределенное поведение, поскольку он ссылается на функцию, которая не определена. В таком случае ваша программа плохо сформирована; диагностика не требуется. Это означает, что GCC может проигнорировать нарушение правил, но другие компиляторы могут диагностировать его или сделать что-то еще странное.

Так что по этим причинам я бы отказался от этого пути.

Мои ощущения не являются достаточной аргументацией, поэтому я сейчас ищу технические детали.

Вы хотите иметь здесь семантику перемещения. Как насчет того, чтобы сделать это явным?

class T;
class C;

struct CMover {
  C *c;
private:
  CMover(C *c):c(c) { }
  friend CMover move(C &c);
};

class C {
public:
    C() { }
    ~C( ) { /*calls delete for all elements in v*/ }

    C(CMover cmove) {
      swap(v, cmove.c->v);
    }

    inline operator CMover();

    // a lot of member functions that modify C
    // a lot of member functions that don't modify C
private:
    C& operator=(C const&); // not copy assignable
    C(C &); // not lvalue copy-constructible

private:
    vector< T* > v;
};

CMover move(C &c) { return CMover(&c); }
C::operator CMover() { return move(*this); }

Теперь вы можете сказать

C init() // for whatever reason object CANNOT be allocated in free memory
{
    C c;
    return move(c);
}

int main() {
  C const c(init());
}
person Johannes Schaub - litb    schedule 05.04.2011
comment
не могли бы вы указать мне, где С++ 03 запрещает ссылаться на неопределенные функции? Что касается семантики перемещения, мы не хотим этого настолько сильно в данном конкретном случае: мой вопрос больше о том, почему это предложение должно быть принято или запрещено. - person Alexander Poluektov; 05.04.2011
comment
@ Alex 3.2p3, предложение следует отклонить, потому что оно не работает переносимо. Я понимаю ваш вопрос, но я решил, что в дополнение к ответу на ваш вопрос я также расскажу, как я его решу. - person Johannes Schaub - litb; 05.04.2011
comment
Спасибо, 3.2/2 + 3.2/3 ясно показывает, что это УБ. Также мы только что выяснили, что MSVC++ 2008 и 2010 (которые не поддерживаются сейчас, но кто знает) отвергают этот код. - person Alexander Poluektov; 06.04.2011

Очевидный ответ заключается в том, что компилятор не обязан исключать конструкцию копии, особенно если оптимизация отключена (хотя g++ по-прежнему пропускает копию даже без оптимизации). Таким образом, вы ограничиваете переносимость.

Скорее всего, если он скомпилируется на конкретном компиляторе, он будет работать так, как ожидалось.

Я знаю, что существует множество, казалось бы, произвольных/искусственных ограничений на то, что вы можете делать, но можете ли вы использовать прокси-держателя для C?

class C
{
private:
    C() { }
    ~C( ) { /*calls delete for all elements in v*/ }

    // ...
    friend class C_Proxy;
};

class C_Proxy
{
public:
    C_Proxy() : c_() { init(c_); }

    // Or create a public const& attribute to point to this.
    const C& get_C() const { return c_; }

private:
    C c_;
};
person Mark B    schedule 05.04.2011

Это похоже на то, что вы не сможете выкинуть из головы только по техническим причинам (например, «Но это компилируется и работает на нашем компиляторе!»), Так что, может быть, концептуально более простой подход может быть хорошей идеей?

Если вас беспокоит постоянство...

C c;
init(c);

// bad: here follows a lot of code that only wants read-only access to c
//      but c cannot be declared const

vs.

C const & c = init();

самое простое (например: никаких взломов и прокси-серверов не требуется) решение может быть:

C c_mutable_object;
init(c_mutable_object);
C const& c = c_mutable_object;

// ... lots of code with read-only access to c
person Martin Ba    schedule 05.04.2011