6

Is there any difference when initializing variables in the contract constructor or directly in the declaration? What is the best practice?

contract A {
   uint public storage = 10000;
   constructor(){}
}

or

contract B {
   uint public storage;
   constructor(){
      storage = 10000;
   }
}

1 Answer 1

8

The place where you initialize each variable determines when the initialization code will run. During initialization the code is executed in the following order:

  1. Expressions used as initializers in state variable declarations (least to most derived).
  2. Base constructor arguments (most to the least derived).
  3. Constructor bodies (least to the most derived).

If the initialization of each variable is independent, this makes no practical difference. You might simply get bytecode that is functionally equivalent but has some instructions ordered slightly differently.

If variables depend on each other, you might actually get different results. In all of these places you can refer to other state variables and call arbitrary functions (even external ones). The function you call can have side-effects or modify state variables (even before they are initialized).

To avoid problems that can arise from this it's best to keep things simple. Avoid dependencies and for declarations and base constructor arguments try to only use simple constants or functions that do not read or write other state variables. If you need to do something more complex, keep that logic inside the constructor body to make it easier to reason about what runs in what order.

Practical examples

Bytecode differences in your example

Your example is a very simple scenario where there's obviously no difference in contract behavior but let's take look at what happens under the hood.

a.sol

contract A {
    uint public s = 10000;
}

b.sol

contract B {
    uint public s;

    constructor() {
        s = 10000;
    }
}

Here's the difference between the assembly produced in both cases:

solc a.sol --optimize --asm --debug-info none > a.asm
solc b.sol --optimize --asm --debug-info none > b.asm
diff --unified a.asm b.asm
@@ -1,10 +1,7 @@

-======= a.sol:A =======
+======= b.sol:B =======
 EVM assembly:
   mstore(0x40, 0x80)
-  0x2710
-  0x00
-  sstore
   callvalue
   dup1
   iszero
@@ -15,6 +12,9 @@
   revert
 tag_1:
   pop
+  0x2710
+  0x00
+  sstore
   dataSize(sub_0)
   dup1
   dataOffset(sub_0)
@@ -66,6 +66,6 @@
       swap1
       return

-    auxdata: 0xa26469706673582212201cfb33d8ca060adae5b385c6039c60778e487b6364ff4076e825b1d418fd206964736f6c634300080a0033
+    auxdata: 0xa264697066735822122027a7491b2543ebe695a4bc9b47f2110e1e14a5212f2c0ae7b5a34cda7f996e3d64736f6c634300080a0033
 }

As you can see, the only real difference is that in the case with no constructor the initialization happens a bit earlier but the end result is still the same.

And if you try the new (still experimental) code generator instead (solc --optimize --ir-optimized) you'll actually end up with the same exact assembly code.

Initialization order

Let's look at an example that illustrates the initialization order:

contract A {
    string[] order;

    constructor(uint _x) {
        log("constructor of A");
    }

    function log(string memory _word) internal returns (uint) {
        order.push(_word);
        return 42;
    }
}

contract B is A {
    uint b = log("declaration of b");

    constructor(uint _x) A(log("args of A")) {
        log("constructor of B");
    }
}

contract C is B {
    uint c = log("declaration of c");

    constructor() B(log("args of B")) {
        log("constructor of C");
    }

    function get() public view returns (string[] memory) {
        return order;
    }
}

If you deploy the code above and run C.get(), you'll see the following values:

  • "declaration of b"
  • "declaration of c"
  • "args of B"
  • "args of A"
  • "constructor of A"
  • "constructor of B"
  • "constructor of C"

Dependencies between variables

The consequence of these rules is that it may be possible to access a variable before it is initialized:

contract C {
    uint a = 1;
    uint b;
    uint c = b + 1;

    constructor() {
        b = 2;
    }

    function get() public view returns (uint, uint, uint) {
        return (a, b, c);
    }
}

In this example running C.get() will give you (1, 2, 1) rather than the (1, 2, 3) you might expect. This is because C.c gets initialized before C.b. Unused storage is always zero-initialized so C.c is assigned 0 + 1.

3
  • Cameel, Do I understand correctly from your answer that: a) init at declaration vs. init in constructor makes little to no difference cost wise. b) init at declaration (in general) is more robust than init in constructor. ?
    – JDOaktown
    Commented Jan 27, 2022 at 18:45
  • 1
    a) Yes. There should be not difference at all. Technically, putting the code at a different spot could result in optimizer making a different decision due to this or losing/gaining some small optimization opportunity but even if that actually happens the effect should be minimal. In practice it's not something I would worry about.
    – cameel
    Commented Jan 27, 2022 at 22:30
  • 1
    b) I wouldn't necessarily say that. In either case you can get unexpected results if the variables depend on each other. And in both cases you have to take into account initialization order if your dependencies go across contracts.
    – cameel
    Commented Jan 27, 2022 at 22:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.