Search code examples
perlpingstrawberry-perl

Why is the Hashref passed to the Net::Ping constructor, set to an empty hashref after Net::Ping->new($args)?


What am I missing here?

When passing arguments to Net::Ping like this, then $args and $args_copy will both be set to an empty hashref after initializing the constructor Net::Ping->new($args).

use strict;
use warnings;
use Data::Dumper qw(Dumper);
use Net::Ping;

sub _ping {
    my ($args) = @_;
    my $p = Net::Ping->new($args);
    $p->close();
}

my $args      = { proto => 'udp' };
my $args_copy = $args;

print Dumper $args;                         # $VAR1 = { 'proto' => 'udp' }
print Dumper $args_copy;                    # $VAR1 = { 'proto' => 'udp' }
_ping($args);
print Dumper $args;                         # $VAR1 = {}
print Dumper $args_copy;                    # $VAR1 = {}

I see the same behavior on both Strawberry Perl and WSL2 running Ubuntu 20.04.4 LTS with Perl v5.30.0.


Solution

  • This is interesting, a class (constructor) deleting caller's data.

    The shown code passes a reference to the Net::Ping constructor and that data gets cleared, and right in the constructor (see below).

    To avoid having $args cleared, if that is a problem, pass its copy instead

    _ping( { %$args } );
    

    This first de-references the hash and then constructs an anonymous hash reference with it, and passes that. So $args is safe.

    The constructor new uses data from @_ directly (without making local copies), and as it then goes through the keys it also deletes them, I presume for convenience in further processing. (I find that scary, I admit.)

    Since a reference is passed to new the data in the calling code can get changed.


    When copying a hash (or array) with a complex data structure in it -- when its values themselves contain references -- we need to make a deep copy. One way is to use Storable for it

    use Storable qw(dclone);
    
    my $deep_copy = dclone $complex_data_structure;
    

    Here that would mean _ping( dclone $args );. It seems that new can only take a reference to a flat hash (or scalars) so this wouldn't be necessary.


    When a sub works directly with the references it gets then it can change data in the caller

    sub sub_takes_ref {
        my ($ref_data) = @_;
    
        for my $k (keys %$ref_data) {   
            $ref_data->{$k} = ...;      # !!! data in caller changed !!!
        }    
    }
    
    ...
    
    my $data = { ... };  # a hashref
    
    sub_takes_ref( $data );
    

    However, if a local copy of arguments is made in the sub then caller's data cannot be changed

    use Storable qw(dclone);  # for deep copy below
    
    sub sub_takes_ref {
        my ($ref_data) = @_;
      
        my $local_copy_of_data = dclone $ref_data;
    
        for my $k (keys %$local_copy_of_data) {   
            $local_copy_of_data->{$k} = ...;   # data in caller safe
        }
    }
    

    (Just remember to not touch $ref_data but to use the local copy.)

    This way of changing data in the caller is of course useful when the sub is meant to work on data structures with large amounts of data, since this way they don't have to be copied. But when that is not the purpose of the sub then we need to be careful, or just make a local copy to be safe.