Dove è memorizzata la variabile "this"?

4

Prendiamo questo semplice programma c ++ come esempio:

#include <vector>
class A
{
  void fun() { a = this + 1; }
  A* a;
};

main()
{
  std::vector<A> vec;
  vec.resize(100);
}

Dimenticando per il momento che this è un puntatore a qualche istanziazione di A , questa domanda riguarda la variabile this stessa, e dove è memorizzato il suo contenuto (il valore dell'indirizzo di memoria).

Non può essere un valore letterale perché il compilatore non può conoscere la posizione della classe A , quindi il suo contenuto deve essere allocato da qualche parte sullo stack o heap come variabile. In questo caso le istanze di A sono allocate nell'heap; significa che il puntatore this si trova anche nell'heap?

Guardando la chiamata di resize di vec , un compilatore potrebbe fare qualcosa di simile sotto il cofano:

std::vector<A>::resize(this, 100); // where 'this' is a pointer to vec

Quindi la mia domanda è, da dove viene il puntatore this ? Vale a dire. dov'è il contenuto di this (il valore dell'indirizzo di memoria a 32/64 bit) memorizzato in memoria per essere passato a questo metodo?

Supporrei che non possa essere un membro normale; quindi il mio primo pensiero è stato che si tratta di una variabile globale mutilata di qualche tipo, ma poi memorizzandone un numero sconosciuto in un vettore non vedo come il compilatore possa ottenere questo risultato. Dov'è il contenuto della variabile this memorizzata in relazione al contenuto del membro della classe A a cui si riferisce?

Se lo standard ha qualcosa da dire su questo, preferirei una risposta con riferimento allo standard.

Si noti che la ragione che voglio sapere è perché sono preoccupato della condivisione errata della memoria allocata per l'oggetto this se modifico le variabili membro. La conoscenza di dove è memorizzato l'oggetto this potrebbe influire sulle decisioni di padding.

    
posta quant 29.08.2014 - 01:54
fonte

7 risposte

17

Il punto è che this è un parametro formale implicito (contenente l'indirizzo dell'oggetto di cui metodo che stai chiamando). Non è una variabile locale.

Guarda il codice generato del tuo programma. Ho compilato (su Linux / Debian / Sid / x86-64 con GCC 4.9.1) il tuo esempio arman.cc con

  gcc -O1 -fverbose-asm -S arman.cc

e ha ottenuto la funzione main sotto

         .globl  main
         .type   main, @function
 main:
 .LFB512:
         .cfi_startproc
         .cfi_personality 0x3,__gxx_personality_v0
         .cfi_lsda 0x3,.LLSDA512
         pushq   %rbx    #
         .cfi_def_cfa_offset 16
         .cfi_offset 3, -16
         subq    $48, %rsp       #,
         .cfi_def_cfa_offset 64
         movq    $0, 16(%rsp)    #, MEM[(struct _Vector_impl *)&vec]._M_start
         movq    $0, 24(%rsp)    #, MEM[(struct _Vector_impl *)&vec]._M_finish
         movq    $0, 32(%rsp)    #, MEM[(struct _Vector_impl *)&vec]._M_end_of_storage
         movq    $0, (%rsp)      #, MEM[(struct A *)&__x]
         movq    %rsp, %rcx      #,
         movl    $100, %edx      #,
         movl    $0, %esi        #,
         leaq    16(%rsp), %rdi  #, tmp92
 .LEHB0:
         call    _ZNSt6vectorI1ASaIS0_EE14_M_fill_insertEN9__gnu_cxx17__normal_iteratorIPS0_S2_EEmRKS0_  #
 .LEHE0:
         movq    16(%rsp), %rdi  # MEM[(struct _Vector_base *)&vec]._M_impl._M_start, D.10014
         testq   %rdi, %rdi      # D.10014
         je      .L34    #,
         call    _ZdlPv  #
         jmp     .L34    #
 .L33:
         movq    %rax, %rbx      #, tmp91
         movq    16(%rsp), %rdi  # MEM[(struct _Vector_base *)&vec]._M_impl._M_start, D.10014
         testq   %rdi, %rdi      # D.10014
         je      .L32    #,
         call    _ZdlPv  #
 .L32:
         movq    %rbx, %rdi      # tmp91,
 .LEHB1:
         call    _Unwind_Resume  #
 .LEHE1:
 .L34:
         movl    $0, %eax        #,
         addq    $48, %rsp       #,
         .cfi_def_cfa_offset 16
         popq    %rbx    #
         .cfi_def_cfa_offset 8
         ret
         .cfi_endproc

Si vede che uno spazio per il tuo vec è allocato nello stack (es. con subq $48, %rsp etc ...) e quindi l'indirizzo di quella zona sullo stack viene passato come argomento formale this (usando le solite convenzioni x86-64 ABI , che dettano (p 20) che il primo argomento di funzione è passato attraverso il registro %rdi ) quindi potresti dire che this è, all'inizio di qualche funzione membro, nel registro %rdi ...

IIRC, la formulazione dello standard C ++ è abbastanza vaga da permettere che l'argomento this venga passato in modo speciale, ma tutti i ABI ho sentito che lo stanno passando esattamente come il primo argomento (puntatore) delle solite funzioni C.

BTW, dovresti fidarti del compilatore e lasciarlo passare this come conveniente e come prescritto dalle specifiche ABI.

    
risposta data 29.08.2014 - 06:58
fonte
12

Il puntatore this non è memorizzato da nessuna parte in relazione all'istanza della classe (perché se hai già l'istanza della classe, ottenere il suo puntatore è banale!). Piuttosto si comporta come un argomento nascosto che è sempre implicitamente passato in funzioni membro come resize . (In alcune lingue come Python questo viene passato esplicitamente.)

Si noti che lo standard non dice nulla su dove this è memorizzato. Non sei autorizzato a prendere l'indirizzo di this perché non è una variabile. Quindi la tua domanda riguarda interamente un dettaglio di implementazione.

Come esempio concreto,

class A
{
public:
  void fun(int x) { a = this + x; }
  A* a;
};

void foo(A& a)
{
  a.fun(1);
}

Questo è semanticamente equivalente al seguente:

struct A
{
  A* a;
};
void A_fun(A* _this, int x) { _this->a = _this + x; }

void foo(A& a)
{
  A_fun(&a, 1);
}

Quindi è possibile per un compilatore implementare le funzioni membro in questo modo (o qualcosa di simile). Tuttavia, il modo in cui un compilatore implementa il programma varia, tuttavia.

    
risposta data 29.08.2014 - 02:10
fonte
2

A dire il vero ci sono vari modi in cui un compilatore può implementare this , e non dovrebbe importarti quale sia effettivamente utilizzato. Sì, potrebbero esserci delle interazioni sottili con il processore che potrebbero avere un impatto sulle prestazioni, ma gli scrittori di compilatori quasi sicuramente ne sanno molto meglio di te.

Ma l'approccio più semplice è semplicemente trattare this come se fosse un qualsiasi altro parametro. Quindi per una classe come:

class Foo
{
    void bar();
    ...
};

Foo foo;
foo.bar();

Il codice prodotto è probabilmente più o meno l'equivalente C di:

struct Foo
{
    ...
}

Foo_bar(Foo * foo) {
}

Foo foo;
Foo_bar(&foo);

Quindi all'interno di Foo :: bar, this deriva semplicemente dall'essere un parametro nascosto per il metodo. A tale riguardo, funziona come qualsiasi altro parametro.

Tuttavia, potresti voler sapere, da dove viene il puntatore in primo luogo? Ad un certo punto nel tempo è stato creato l'oggetto Foo . Potrebbe essere stato creato come variabile locale, o forse tramite new . In entrambi i casi, il programma inizia la creazione di Foo selezionando il pezzo di memoria per creare l'oggetto. Conosce l'indirizzo corretto di Foo, perché ha selezionato o ricevuto il pezzo di memoria in cui inserire Foo. Semplicemente tiene traccia di quell'indirizzo per quando ha bisogno di quel puntatore in seguito.

    
risposta data 29.08.2014 - 06:48
fonte
2

Arman, esaminando le tue domande, vorrei sottolineare un aspetto che penso manchi. Il tuo istinto che il puntatore this debba essere memorizzato da qualche parte è assolutamente giusto. Ma il tuo errore consiste nel limitare le scelte a "accumulare" e "impilare". Questi sono certamente luoghi comuni per i dati da memorizzare, ma ce ne sono altri (e ce ne sono altri oltre a questi!):

  • Registri CPU
  • "la sezione globale"
  • archiviazione locale dei thread

Quando gli argomenti vengono passati a una funzione, di solito i primi argomenti di "coppia" vengono posizionati nei registri della CPU. I dettagli variano a seconda di molte cose: l'architettura del processore (x86 vs ARM vs ARM64 vs ...), il numero di argomenti, i tipi e le dimensioni degli argomenti (interi, puntatori, float, tipi di vettore) e su determinati codici annotazioni che la funzione potrebbe avere (con cui raramente dovrai fare i conti).

In quasi tutti i processori per i quali è probabile la programmazione, il puntatore "this" verrà passato come 0 ° argomento implicito di una funzione membro, il che significa che verrà passato tramite un registro CPU. In x86_64 che è registrato ECX e in ARM che sarebbe registro r0.

    
risposta data 04.09.2014 - 09:06
fonte
1

Dipende dalla convenzione di chiamata del compilatore ma solitamente viene passato come primo argomento (nascosto) e non memorizzato in A ovunque. Il primo argomento viene quindi passato come registro o nello stack.

    
risposta data 29.08.2014 - 02:10
fonte
1

Annotazione del tuo esempio:

main()
{
  std::vector<A> vec;
  vec.resize(100); // What really happens: vec.resize(&vec, 100);
}

Il compilatore emette il codice che calcola l'indirizzo dell'istanza di classe vec e lo passa come primo parametro a qualsiasi chiamata di metodo contro tale istanza. Non esiste una variabile globale "questo". Non esiste una variabile membro 'this', c'è solo un indirizzo passato come parametro formale a una chiamata di metodo. Puoi fare riferimento ad esso nel tuo corpo del metodo come qualsiasi altro parametro.

    
risposta data 29.08.2014 - 06:21
fonte
-2

Sembra che tu voglia sapere dove sono archiviati i contenuti di un'istanza della classe. La risposta è, dipende.

Le classi possono essere istanziate nello stack, nell'heap o indirettamente nell'heap come nell'esempio di vettore.

Nell'esempio del vettore, dopo vec.resize(100) , ci sono 100 istanze di classe A costruite nella memoria riservata dal vettore vec. La memoria per il vettore vec è allocata nell'heap. Quindi, le istanze di a sono memorizzate indirettamente nell'heap.

Informazioni su this

this è semplicemente l'indirizzo di un'istanza della classe. Possono esserci molte istanze di una classe. Nel tuo esempio, ci sono 100 istanze di classe A, ciascuna con il proprio this . Il this per un'istanza non viene memorizzato da nessuna parte. È solo l'indirizzo dell'istanza.

Ad esempio, chiama vec[3].fun() e il membro a dell'istanza di classe A nella terza posizione del vettore vec sarà un puntatore all'istanza della classe A nella quarta posizione del vettore vec . (Naturalmente, a tua volta, puoi chiamare fun() utilizzando la variabile membro a .)

Il valore di this , all'interno di una chiamata di metodo è determinato dal chiamante del metodo. Il puntatore all'istanza al momento della chiamata al metodo diventa il puntatore this alla chiamata al metodo.

    
risposta data 29.08.2014 - 05:31
fonte

Leggi altre domande sui tag