Фабричный шаблон с абстрактными базовыми классами - возврат по ссылке или по значению? Проблемы с областью действия и нарезкой

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

Как бы я ни пытался это сделать, я сталкиваюсь с проблемами, которые, как я подозреваю, связаны либо с нарезкой, если я возвращаюсь по значению, либо с областью видимости, если я возвращаюсь по ссылке.

В приведенной ниже программе, например, segfaults на линии a.doA() внутри C::doStuff(). Если вместо этого я изменю вызов с value_C_factory<C>() на ref_C_factory<C>(), я получу пару предупреждений о «возвращении ссылки на временную», но программа компилируется, вместо этого segfaults на b.doB() в следующей строке (без вывода чего-либо из a.doA().. .).

След от gdb выглядит так: вторая строка — это та, что в моем коде, упомянутом выше.

#0  0x00007ffff7dbddb0 in vtable for std::ctype<char> () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#1  0x00000000004010e9 in C::doStuff (this=0x7fffffffdd00) at syntax.cpp:57
#2  0x0000000000400cf2 in main () at syntax.cpp:95

Что вызывает эти segfaults? Это, как я подозреваю, нарезка/обзор в случае значения/ссылки? Если нет, то что это? И самое главное, как правильно построить свои экземпляры из входных данных?

Пример кода

Приведенный ниже код должен скомпилироваться и дать указанное выше поведение, например. GCC 4.8 с использованием
gcc -g -Wall -std=c++11 -o test test.cpp (во всяком случае, я так делаю).

#include <iostream>
#include <typeinfo>

class IA {
public:
    virtual void doA() const = 0;
    virtual ~IA() { }
};

class A : public IA {
private:
    std::string atask;
public:
    explicit A(const std::string &task) : atask(task) {
        std::cout << "Created A with task " << atask << std::endl;
    }

    void doA() const {
        std::cout << "I did A! " << atask << std::endl;
    }
};

class IB {
public:
    virtual void doB() const = 0;
    virtual ~IB() { }
};

class B : public IB {
private:
    std::string btask;
public:
    explicit B(const std::string &task) : btask(task) {
        std::cout << "Created B with task " << btask << std::endl;
    }

    void doB() const {
        std::cout << "I did B! " << btask << std::endl;
    }
};

class IC {
public:
    void doStuff() const;
    virtual ~IC() { }
};

class C : public IC {
private:
    const IA &a;
    const IB &b;

public:
    C(const IA &a, const IB &b) : a(a), b(b) { }

    void doStuff() const {
        a.doA();    // with value factory method, segfault here
        b.doB();    // with reference factory, segfault here instead
    }
};

template<typename TA>
TA value_A_factory() {
    return TA("a value");
}

template<typename TB>
TB value_B_factory() {
    return TB("b value");
}

template<typename TC>
TC value_C_factory() {
    return TC(value_A_factory<A>(), value_B_factory<B>());
}


template<typename TA>
const TA &ref_A_factory() {
    return TA("a ref");
}

template<typename TB>
const TB &ref_B_factory() {
    return TB("b ref");
}

template<typename TC>
const TC &ref_C_factory() {
    const TC &c(ref_A_factory<A>(), ref_B_factory<B>());
    return c;
}

int main() {
    C c = value_C_factory<C>();
    std::cout << typeid(c).name() << std::endl;
    c.doStuff();
}

person Tomas Aschan    schedule 02.04.2014    source источник
comment
@JoachimPileborg: У меня сложилось впечатление, что если я верну const ref, все будет в порядке. Но признаю, что я тут совсем запутался, так что вы, скорее всего, более правы, чем я. Однако, если я не по ссылке, не будет ли аргумент нарезан при передаче конструктору C?   -  person Tomas Aschan    schedule 02.04.2014
comment
Вы правы в том, что будет нарезка, но вы ошибаетесь в том, как const& продлевает срок службы временного объекта. Это до конца выражения, в котором оно встречается (в вашем случае конструктор). После этого выражения временные файлы будут уничтожены, а ваши ссылки в C будут болтаться.   -  person pmr    schedule 02.04.2014


Ответы (2)


У вас есть две проблемы, обе вызваны неопределенным поведением.

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

Другая проблема заключается в том, что вы храните ссылки на временные значения. Когда вы создаете свой класс C, такой как TC(value_A_factory<A>(), value_B_factory<B>()), значения, возвращаемые функциями value_X_factory, являются временными и будут уничтожены после завершения выражения (TC(...)).

person Some programmer dude    schedule 02.04.2014
comment
В ПОРЯДКЕ. Так что эталонный подход, вероятно, мертворожденный. Есть ли способ избежать проблем с нарезкой при возврате по значению? - person Tomas Aschan; 02.04.2014
comment
Это не совсем правильно. Возврат константной ссылки отличается от неконстантной ссылки. Если я правильно помню, это можно было использовать в C++03, чтобы избежать копирования экземпляров класса. - person Danvil; 02.04.2014
comment
@TomasLycken Указатели? Вы можете прочитать о std::unique_ptr. - person Some programmer dude; 02.04.2014
comment
Ссылки на константы @Danvil могут продлить время жизни, но не дальше конца выражения, в котором участвует ссылка. - person Some programmer dude; 02.04.2014
comment
@JoachimPileborg: Да, он может передать его по константной ссылке (даже временной), но в какой-то момент ему придется ее сохранить. - person Danvil; 02.04.2014

In

template<typename TA>
const TA &ref_A_factory() {
    return TA("a ref");
}

возврат ссылки на локальную переменную является неопределенным поведением.

In

TC(value_A_factory<A>(), ...)

время жизни значения, возвращаемого value_A_factory, будет концом выражения TC(...). После этого ваши ссылки в C болтаются.

Если вы действительно хотите использовать интерфейсы и фабрики для полиморфных типов, реальной альтернативы динамическому распределению памяти и какой-либо схеме владения не существует. Самым простым было бы для C просто взять на себя ответственность за своих членов и позаботиться об их удалении.

#include <memory>
#include <cassert>

struct IA { 
  virtual void doA() const = 0;
  virtual ~IA() { }; 
};

struct A : IA { 
  void doA() const override {}
};

struct C {
  /* C assumes ownership of a. a cannot be null. */
  C(IA* a) : a{a} { assert(a && "C(IA* a): a was null"); };
private:
  std::unique_ptr<IA> a;
};

C factory() {
  return C{new A};
}

int main()
{
  C c = factory();
  return 0;
}
person pmr    schedule 02.04.2014
comment
Возврат ссылки на локальную переменную является поведением undefined. это неверно для константных ссылок! - person Danvil; 02.04.2014
comment
@ Данвил Ты ошибаешься. Даже возврат константной ссылки на локальную переменную является поведением undefined. Для этого есть даже проклятое предупреждение. ideone.com/8tR6Q7 - person pmr; 02.04.2014
comment
@Danvil И этот stackoverflow.com/questions/1465851/ - person pmr; 02.04.2014
comment
Я вижу свою ошибку сейчас. В стандарте говорится, что временная привязка к возвращаемому значению в операторе возврата функции (6.6.3) сохраняется до тех пор, пока функция не выйдет. Я думал, что в случае константной ссылки она сохраняется до тех пор, пока возвращаемое значение не будет обработано во внешнем выражении. . - person Danvil; 02.04.2014
comment
@Danvil Что-то не так с этим ответом или почему вы оставляете отрицательный голос? - person pmr; 02.04.2014
comment
Не могу отменить, потому что прошел 1 час :( - person Danvil; 02.04.2014
comment
Исправил очень незначительную опечатку - теперь я могу плюсовать :) (это как-то глупо...) - person Danvil; 02.04.2014
comment
В ПОРЯДКЕ. Я надеялся избежать необходимости делать это с указателями. Вы говорите, если вы действительно хотите использовать интерфейсы и фабрики для полиморфных типов — какие еще есть варианты? Является ли этот вопрос действительно XY-проблемой? - person Tomas Aschan; 02.04.2014
comment
@TomasLycken В некоторых случаях вы, вероятно, можете проектировать вокруг них, но достаточно часто они являются прямым решением. Если вы уверены в своем дизайне, используйте предложенное мной решение. - person pmr; 02.04.2014