Можем ли мы вернуть объекты с удаленным/приватным конструктором копирования/перемещения по значению из функции?

В C++03 невозможно вернуть объект класса, имеющего приватный неопределенный конструктор копирования по значению:

struct A { A(int x) { ... } private: A(A const&); };

A f() {
  return A(10); // error!
  return 10;    // error too!
}

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


person Johannes Schaub - litb    schedule 28.10.2011    source источник
comment
Что значит вернуть объект по значению... без его копирования или перемещения?   -  person Richard    schedule 29.10.2011
comment
@Richard, чтобы иметь определение функции, которое не возвращает void или ссылку. Я имею в виду функцию, которая возвращает новый объект класса. Я уточню.   -  person Johannes Schaub - litb    schedule 29.10.2011
comment
@Xeo: Потому что это глупый вопрос от того, кто знает лучше. И я говорю это как человек, который знал/уважал литб до того, как я узнал ТАКОЕ. ;-]   -  person ildjarn    schedule 29.10.2011
comment
@ildjarn о, как я чувствую любовь. Я нашел решение этой викторины ниже!   -  person Johannes Schaub - litb    schedule 29.10.2011
comment
@Johannes: Достаточно честно!   -  person ildjarn    schedule 29.10.2011


Ответы (5)


Вот как это может работать

A f() {
  return { 10 };
}

Это работает даже несмотря на то, что у A нет рабочей копии или конструктора перемещения и нет другого конструктора, который мог бы копировать или перемещать A!

Чтобы использовать эту функцию C++11, конструктор (в данном случае берущий int) должен быть неявным.

person Johannes Schaub - litb    schedule 29.10.2011
comment
Вы и ваши вопросы-викторины. :( - person Xeo; 29.10.2011
comment
Он не компилируется с g++ -std=c++0x. Кстати, а как это можно назвать? - person 6502; 29.10.2011
comment
Учитывая, что ответ Дэвида, похоже, указывает на то, что это все еще должно быть незаконным, не могли бы вы также опубликовать соответствующий стандарт, подтверждающий ваш ответ? - person ildjarn; 29.10.2011
comment
@ildjarn ой, как-то я пропустил эти комментарии. Это указано в 8.5.4p3. Обратите внимание, что мы инициализируем A (объект, представляющий возвращаемое значение) с помощью { 10 }. Здесь нет копирования и перемещения. - person Johannes Schaub - litb; 02.11.2011
comment
Комментарии @6502 о неожиданном поведении программного обеспечения бесполезны, если вы не указываете номера версий. Я использую транк GCC4.7 (4.7.0 20110929), который не показывает описанного вами поведения. - person Johannes Schaub - litb; 02.11.2011
comment
@Johannes: я не вижу в §8.5.4/3 ничего, что указывало бы на то, что это законно. Единственная релевантная часть абзаца, кажется, В противном случае, если T является типом класса, учитываются конструкторы. Что я упустил? То есть, чем ваш код отличается от A f() { return A(10); }? - person ildjarn; 03.11.2011
comment
@ildjarn точно, конструкторы рассматриваются, перечисляются, и лучший из них выбирается по разрешению перегрузки, а затем вызывается для создания объекта возвращаемого значения. Чтобы увидеть, чем он отличается от return 10, вы можете сравнить описание 8.5.4/3 с пунктом 6 подпункта 2 пункта 8.5/16. Как заметит жадный читатель, return 10 копирует временный объект A в другой A. объект (пункт назначения). И return A(10) делает то же самое - временный A объект, который будет скопирован, даже создается явно. - person Johannes Schaub - litb; 04.11.2011
comment
Вам не нужно находить что-то, что прямо заявляет, что это законно. Также нет явного правила, согласно которому преобразование 3 в long допустимо, но из правила следует, что преобразование произвольного int в long допустимо. - person Johannes Schaub - litb; 04.11.2011
comment
Спасибо за пояснение, кажется, теперь я наконец понял. ;-] - person ildjarn; 04.11.2011
comment
Влияет ли на это гарантированное удаление копии С++ 17? - person Rusty Shackleford; 18.05.2017

Ограничение не снято. Что касается спецификатора доступа, в §12.8/32 есть примечание, в котором объясняется:

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

Что касается удаленных конструкторов копирования/перемещения, в §8.4.3/2 говорится, что

Программа, которая явно или неявно ссылается на удаленную функцию, кроме ее объявления, является некорректной. [Примечание: это включает неявный или явный вызов функции и формирование указателя или указателя на член функции. Это применимо даже для ссылок в выражениях, которые потенциально не оцениваются. Если функция перегружена, на нее ссылаются, только если функция выбрана разрешением перегрузки. — примечание в конце]

Не уверен в этом конкретном случае, но мое понимание цитаты таково, что если после разрешения перегрузки в §12.8/32 выбран удаленный конструктор копирования/перемещения, даже если операция опущена, это может представлять собой ссылку к функции, и программа будет неправильно сформирована.

person David Rodríguez - dribeas    schedule 28.10.2011

Приведенный выше код по-прежнему плохо сформирован в C++11. Но вы можете добавить в A конструктор общедоступного перемещения, и тогда это будет законно:

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

A f() {
  return A(10); // Ok!
}
person Howard Hinnant    schedule 28.10.2011
comment
Привет! Извините, я должен был повторить то, что у меня было в заголовке, и в теле вопроса. Сделаем так сейчас. Но это хорошее замечание в любом случае! - person Johannes Schaub - litb; 29.10.2011

Мне было интересно, снято ли это ограничение в С++ 11?

Как это могло произойти? Возвращая что-то по значению, вы по определению копируете (или перемещаете) это. И хотя C++ может позволить исключить это копирование/перемещение в определенных обстоятельствах, он по-прежнему копирует (или перемещает) в соответствии со спецификацией.

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

да. Вы избавляетесь от конструктора/назначения копирования, но разрешаете перемещать значение. std::unique_ptr делает это.

Вы можете вернуть unique_ptr по значению. Но при этом вы возвращаете «prvalue»: временное, которое уничтожается. Поэтому, если у вас есть функция g как таковая:

std::unique_ptr<SomeType> g() {...}

Ты можешь это сделать:

std::unique_ptr<SomeType> value = g();

Но не это:

std::unique_ptr<SomeType> value1 = g();
std::unique_ptr<SomeType> value2 = g();
value1 = value 2;

Но это возможно возможно:

std::unique_ptr<SomeType> value = g();
value = g();

Вторая строка вызывает оператор присваивания перемещения для value. Он удалит старый указатель и переместит в него новый указатель, оставив старое значение пустым.

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

При удалении конструкторов копирования и перемещения создается неподвижный объект. Там, где он создается, его ценности остаются навсегда. Движение позволяет вам иметь уникальную собственность, но не быть неподвижным.

person Nicol Bolas    schedule 28.10.2011
comment
Мне нравится, что вы выделяете unique_ptr. Но ваша строка, содержащая комментарий: //Copy assignment is forbidden., неверна. Это задание на перемещение, и unique_ptr разрешает его. - person Howard Hinnant; 29.10.2011
comment
@HowardHinnant: я думал, что удалил эту часть, прежде чем опубликовать ее (поэтому позже я покажу, что она работает), но я не удалил ее полностью. - person Nicol Bolas; 29.10.2011
comment
g() — это значение prvalue, а не значение x. Если g были объявлены, например. std::unique_ptr<SomeType>&& g();, то g() будет xvalue. Обратите внимание, что для ваших примеров в любом случае достаточно, чтобы g() было значением r. - person Luc Danton; 29.10.2011

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

Что-то вроде:

template<typename T>
struct ReturnProxy {
    //This could be made private, provided appropriate frienship is granted
    ReturnProxy(T* p_) : p(p_) { }
    ReturnProxy(ReturnProxy&&) = default;

private:
    //don't want these Proxies sticking around...
    ReturnProxy(const ReturnProxy&) = delete;
    void operator =(const ReturnProxy&) = delete;
    void operator =(ReturnProxy&&) = delete;

    struct SUPER_FRIENDS { typedef T GO; };
    friend struct SUPER_FRIENDS::GO;
    unique_ptr<T> p;
};

struct Object {
    Object() : data(0) { }

    //Pseudo-copy constructor
    Object(ReturnProxy<Object>&& proxy)
      : data(proxy.p ? proxy.p->data : throw "Don't get sneaky with me \\glare") 
    {
      //steals `proxy.p` so that there isn't a second copy of this object floating around
      //shouldn't be necessary, but some men just want to watch the world burn.
      unique_ptr<Object> thief(std::move(proxy.p));
    }
private:
    int data;

    Object(const Object&) = delete;
    void operator =(const Object&) = delete;
};

ReturnProxy<Object> func() {
    return ReturnProxy(new Object);
}

int main() {
    Object o(func());
}

Вы, вероятно, могли бы сделать то же самое в 03, используя auto_ptrs. И это, очевидно, не препятствует хранению результирующего Object, хотя и ограничивает вас одной копией на экземпляр.

person Dennis Zickefoose    schedule 29.10.2011
comment
Я пытался найти способ избавиться от общедоступного конструктора перемещения из ReturnProxy, но потом понял, что это исходный вопрос, просто переместился на уровень назад. - person Dennis Zickefoose; 29.10.2011
comment
Даже если бы вы это сделали, я мог бы просто сохранить прокси по значению, используя auto. auto может объявлять частные типы и тому подобное. - person Nicol Bolas; 29.10.2011