Vida útil garantida de temporário em C ++?

103

C ++ fornece uma garantia para a vida útil de uma variável temporária que é criada dentro de uma chamada de função, mas não usada como um parâmetro? Aqui está um exemplo de aula:

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;
};

E aqui está como você o usaria:

// 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);

Quando o destruidor do objeto StringBuffer temporário será chamado? É isso:

  • Antes da chamada para GetString?
  • Depois que GetString retornar?
  • Depende do compilador?

Eu sei que C ++ garante que uma variável temporária local será válida, desde que haja uma referência a ela - isso se aplica a objetos pai quando há uma referência a uma variável de membro?

Obrigado.

Mark Ransom
fonte
por que não herdar e sobrecarregar ou fazer uma função global? Eu ficaria mais limpo e você não teria que criar uma classe apenas para chamar um membro.
Jacek Ławrynowicz
1
Se você estiver indo para usar isso, você deve chamar m_str.reserve(maxlength)em char * Size(int maxlength), caso contrário, o destruidor poderia jogar.
Mankarse,

Respostas:

109

O destruidor para esse tipo de temporários é chamado no final da expressão completa. Essa é a expressão mais externa que não faz parte de nenhuma outra expressão. É o seu caso depois que a função retorna e o valor é avaliado. Então, vai funcionar muito bem.

Na verdade, é o que faz os modelos de expressão funcionarem: eles podem manter referências a esse tipo de temporário em uma expressão como

e = a + b * c / d

Porque todo temporário vai durar até a expressão

x = y

É avaliado completamente. É descrito de forma bastante concisa 12.2 Temporary objectsno Padrão.

Johannes Schaub - litb
fonte
3
Eu nunca cheguei a conseguir uma cópia do padrão. Eu deveria fazer disso uma prioridade.
Mark Ransom
2
@JohannesSchaub: O que é uma "expressão completa" neste caso printf("%s", strdup(std::string("$$$").c_str()) );:? Quer dizer, se strdup(std::string("$$$").c_str())for considerada a expressão completa, então o ponteiro que strdupvê é válido . Se std::string("$$$").c_str()for uma expressão completa, o ponteiro que strdupvê é inválido ! Você poderia explicar um pouco mais com base neste exemplo?
Grim Fandango
2
@GrimFandango AIUI seu todo printfé a expressão completa. Portanto, strdupé um vazamento de memória desnecessário - você pode apenas deixá-lo imprimir c_str()diretamente.
Josh Stone
1
"Esse tipo de temporário" - Que tipo é esse? Como posso saber se meu temporário é "aquele tipo" de temporário?
RM
@RM bastante justo. Eu quis dizer "aqueles que você cria dentro de um argumento de função", como é feito na pergunta.
Johannes Schaub - litb
18

a resposta de litb é correta. O tempo de vida do objeto temporário (também conhecido como rvalue) está vinculado à expressão e o destruidor do objeto temporário é chamado no final da expressão completa e quando o destruidor em StringBuffer é chamado, o destruidor em m_buffer também será chamado, mas não o destruidor em m_str, pois é uma referência.

Observe que C ++ 0x muda um pouco as coisas porque adiciona referências rvalue e move a semântica. Essencialmente, usando um parâmetro de referência rvalue (notado com &&), posso 'mover' o rvalue para a função (em vez de copiá-lo) e o tempo de vida do rvalue pode ser vinculado ao objeto para o qual ele se move, não à expressão. Há uma postagem de blog muito boa da equipe do MSVC sobre isso detalhadamente e incentivo as pessoas a lê-la.

O exemplo pedagógico para mover rvalue são strings temporárias e mostrarei a atribuição em um construtor. Se eu tiver uma classe MyType que contém uma variável de membro de string, ela pode ser inicializada com um rvalue no construtor assim:

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

Isso é bom porque quando eu declaro uma instância desta classe com um objeto temporário:

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

o que acontece é que evitamos copiar e destruir o objeto temporário e "hello" é colocado diretamente dentro da variável de membro da instância da classe proprietária. Se o objeto for mais pesado do que uma 'string', a cópia extra e a chamada do destruidor podem ser significativas.

Rick
fonte
1
Para fazer a movimentação funcionar, acho que você precisa descartar const e usar std :: move como MyType (std :: string && name): m_name (std :: move (name)) {}
gast128
4

Após o retorno da chamada para GetString.

David Segonds
fonte
3

StringBuffer está no escopo de GetString. Ele deve ser destruído no final do escopo de GetString (ou seja, quando ele retorna). Além disso, não acredito que C ++ garanta que uma variável existirá enquanto houver referência.

O seguinte deve ser compilado:

Object* obj = new Object;
Object& ref = &(*obj);
delete obj;
BigSandwich
fonte
Acho que exagerei na garantia - é apenas para temporários locais. Mas existe.
Mark Ransom
Eu editei a pergunta. Com base nas respostas fornecidas até agora, parece ser um ponto discutível.
Mark Ransom
Ainda não acho que sua edição esteja correta: Object & obj = GetObj (); Object & GetObj () {return & Object (); } // ruim - deixará a referência pendente.
BigSandwich
1
Obviamente, estou fazendo um péssimo trabalho ao me explicar, e posso não entender 100% também. Veja informit.com/guides/content.aspx?g=cplusplus&seqNum=198 - ele explica e responde também à minha pergunta original.
Mark Ransom
1
Obrigado, pelo link, isso faz sentido agora.
BigSandwich
3

Escrevi quase exatamente a mesma aula:

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); }

Antes do padrão, cada compilador fazia isso de maneira diferente. Eu acredito que o antigo Manual de Referência Anotado para C ++ especificava que os temporários deveriam limpar no final do escopo, então alguns compiladores faziam isso. No final de 2003, descobri que o comportamento ainda existia por padrão no compilador Forte C ++ da Sun, então StringBuffer não funcionava. Mas eu ficaria surpreso se algum compilador atual ainda estivesse tão quebrado.

Daniel Earwicker
fonte
Assustador como eles são semelhantes! Obrigado pelo aviso - o primeiro lugar que tentarei é o VC ++ 6, que não é conhecido por sua conformidade com os padrões. Estarei observando com atenção.
Mark Ransom
Eu teria escrito a classe originalmente em VC ++ 6, então não deveria ser um problema.
Daniel Earwicker