Durée de vie garantie du temporaire en C ++?

103

Le C ++ fournit-il une garantie pour la durée de vie d'une variable temporaire créée dans un appel de fonction mais non utilisée comme paramètre? Voici un exemple de classe:

class StringBuffer
{
public:
    StringBuffer(std::string & str) : m_str(str)
    {
        m_buffer.push_back(0);
    }
    ~StringBuffer()
    {
        m_str = &m_buffer[0];
    }
    char * Size(int maxlength)
    {
        m_buffer.resize(maxlength + 1, 0);
        return &m_buffer[0];
    }
private:
    std::string & m_str;
    std::vector<char> m_buffer;
};

Et voici comment vous l'utiliseriez:

// this is from a crusty old API that can't be changed
void GetString(char * str, int maxlength);

std::string mystring;
GetString(StringBuffer(mystring).Size(MAXLEN), MAXLEN);

Quand le destructeur de l'objet StringBuffer temporaire sera-t-il appelé? Est-ce:

  • Avant l'appel à GetString?
  • Après le retour de GetString?
  • Dépendant du compilateur?

Je sais que C ++ garantit qu'une variable temporaire locale sera valide tant qu'il y aura une référence à elle - cela s'applique-t-il aux objets parents lorsqu'il y a une référence à une variable membre?

Merci.

Mark Ransom
la source
pourquoi ne pas hériter et surcharger ou faire une fonction globale? je serais plus propre et vous n'auriez pas à créer une classe seulement pour appeler un membre.
Jacek Ławrynowicz
1
Si vous allez utiliser, vous devez appeler m_str.reserve(maxlength)à char * Size(int maxlength), sinon le destructeur pourrait jeter.
Mankarse

Réponses:

109

Le destructeur pour ce type de temporaires est appelé à la fin de l'expression complète. C'est l'expression la plus externe qui ne fait partie d'aucune autre expression. C'est dans votre cas après le retour de la fonction et l'évaluation de la valeur. Donc, tout fonctionnera bien.

C'est en fait ce qui fait fonctionner les modèles d'expression: ils peuvent conserver des références de conservation à ce type de temporels dans une expression comme

e = a + b * c / d

Parce que chaque temporaire durera jusqu'à l'expression

x = y

Est évalué complètement. Il est décrit de manière assez concise 12.2 Temporary objectsdans la norme.

Johannes Schaub - litb
la source
3
Je n'ai jamais vraiment réussi à obtenir une copie de la norme. Je devrais en faire une priorité.
Mark Ransom
2
@JohannesSchaub: Qu'est-ce qu'une "expression complète" dans ce cas:? printf("%s", strdup(std::string("$$$").c_str()) );Je veux dire si strdup(std::string("$$$").c_str())est considérée comme l'expression complète, alors le pointeur qui strdupvoit est valide . Si std::string("$$$").c_str()est une expression complète, alors le pointeur qui strdupvoit n'est pas valide ! Pourriez-vous s'il vous plaît expliquer un peu plus en vous basant sur cet exemple?
Grim Fandango
2
@GrimFandango AIUI votre tout printfest l'expression complète. Il strdups'agit donc d'une fuite de mémoire inutile - vous pouvez simplement le laisser imprimer c_str()directement.
Josh Stone
1
"Ce genre de temporaires" - De quel genre s'agit-il? Comment puis-je savoir si mon temporaire est "ce genre" de temporaire?
RM
@RM assez juste. Je voulais dire "ceux que vous créez dans un argument de fonction", comme cela se fait dans la question.
Johannes Schaub - litb
18

La réponse de litb est exacte. La durée de vie de l'objet temporaire (également appelée rvalue) est liée à l'expression et le destructeur de l'objet temporaire est appelé à la fin de l'expression complète et lorsque le destructeur sur StringBuffer est appelé, le destructeur sur m_buffer sera également appelé, mais pas le destructeur sur m_str puisqu'il s'agit d'une référence.

Notez que C ++ 0x change un peu les choses car il ajoute des références rvalue et déplace la sémantique. Essentiellement en utilisant un paramètre de référence rvalue (noté avec &&), je peux `` déplacer '' la rvalue dans la fonction (au lieu de la copier) et la durée de vie de la rvalue peut être liée à l'objet dans lequel elle se déplace, pas à l'expression. Il y a un très bon article de blog de l'équipe MSVC sur ce sujet en détail et j'encourage les gens à le lire.

L'exemple pédagogique pour déplacer les rvalue est des chaînes temporaires et je vais montrer l'affectation dans un constructeur. Si j'ai une classe MyType qui contient une variable membre de chaîne, elle peut être initialisée avec une rvalue dans le constructeur comme ceci:

class MyType{
   const std::string m_name;
public:
   MyType(const std::string&& name):m_name(name){};
}

C'est bien car lorsque je déclare une instance de cette classe avec un objet temporaire:

void foo(){
    MyType instance("hello");
}

ce qui se passe, c'est que nous évitons de copier et de détruire l'objet temporaire et "hello" est placé directement dans la variable membre de l'instance de classe propriétaire. Si l'objet est plus lourd qu'une «chaîne», l'appel de copie et de destructeur supplémentaire peut être significatif.

Meule
la source
1
Pour que le déplacement fonctionne, je pense que vous devez supprimer le const et utiliser std :: move comme MyType (std :: string && name): m_name (std :: move (name)) {}
gast128
4

Après le retour de l'appel à GetString.

David Segonds
la source
3

StringBuffer est dans la portée de GetString. Il devrait être détruit à la fin de la portée de GetString (c'est-à-dire quand il revient). De plus, je ne crois pas que C ++ garantira qu'une variable existera tant qu'il y aura référence.

Les éléments suivants doivent être compilés:

Object* obj = new Object;
Object& ref = &(*obj);
delete obj;
BigSandwich
la source
Je pense que j'ai exagéré la garantie - c'est pour les temporaires locaux seulement. Mais cela existe.
Mark Ransom
J'ai édité la question. Sur la base des réponses fournies jusqu'à présent, cela semble toutefois être un point discutable.
Mark Ransom
Je ne pense toujours pas que votre modification soit correcte: Object & obj = GetObj (); Object & GetObj () {return & Object (); } // mauvais - laissera une référence en suspens.
BigSandwich
1
Je suis évidemment en train de mal m'expliquer et je ne comprends peut-être pas à 100% non plus. Regardez informit.com/guides/content.aspx?g=cplusplus&seqNum=198 - il explique et répond également à ma question initiale.
Mark Ransom le
1
Merci, pour le lien, cela a du sens maintenant.
BigSandwich
3

J'ai écrit presque exactement la même classe:

template <class C>
class _StringBuffer
{
    typename std::basic_string<C> &m_str;
    typename std::vector<C> m_buffer;

public:
    _StringBuffer(std::basic_string<C> &str, size_t nSize)
        : m_str(str), m_buffer(nSize + 1) { get()[nSize] = (C)0; }

    ~_StringBuffer()
        { commit(); }

    C *get()
        { return &(m_buffer[0]); }

    operator C *()
        { return get(); }

    void commit()
    {
        if (m_buffer.size() != 0)
        {
            size_t l = std::char_traits<C>::length(get());
            m_str.assign(get(), l);    
            m_buffer.resize(0);
        }
    }

    void abort()
        { m_buffer.resize(0); }
};

template <class C>
inline _StringBuffer<C> StringBuffer(typename std::basic_string<C> &str, size_t nSize)
    { return _StringBuffer<C>(str, nSize); }

Avant le standard, chaque compilateur le faisait différemment. Je crois que l'ancien manuel de référence annoté pour C ++ spécifiait que les temporaires devraient nettoyer à la fin de la portée, c'est pourquoi certains compilateurs l'ont fait. En 2003, j'ai trouvé que le comportement existait toujours par défaut sur le compilateur Forte C ++ de Sun, donc StringBuffer ne fonctionnait pas. Mais je serais étonné si un compilateur actuel était toujours aussi cassé.

Daniel Earwicker
la source
Spooky comme ils sont similaires! Merci pour l'avertissement - le premier endroit où je vais l'essayer est VC ++ 6, qui n'est pas connu pour sa conformité aux normes. Je vais regarder attentivement.
Mark Ransom
J'aurais écrit la classe à l'origine sur VC ++ 6 donc cela ne devrait pas être un problème.
Daniel Earwicker le