Nelfeal
Nelfeal

Reputation: 13269

"immutable" struct with non-trivial constructor

I have been looking for a way to declare some sort of immutable type with a non-trivial constructor. My current goal is to read data from a file to construct an object so that it cannot be modified subsequently. It resembles a POD type, except I need data from a file, so the constructor has to read it.

Through my research and experiments, I have thought of three ways to do just that. Basically, my question is : is there any better way to do what I want ?

In the following exemple codes, I will use std::cin as a substitute for the file. First off, here is the obvious class-with-getters way :

class A {
public:
    A() { std::cin >> m_i; }
    int i() { return m_i; }

private:
    int m_i;
};

As a matter of fact, I am having trouble with this solution, simply because of the getter(s). After all, it is kind of a POD type, and I would like it to be treated as such, with public data members. Also, I just don't like getters. So I tried it with some const-ness and by tweaking the constructor :

struct B {
    B() : B(B::fromFile()) {
    }

    B(int i) : i(i) {
    }

    const int i;

private:
    static B fromFile() {
        int i;
        std::cin >> i;
        return B(i);
    }
};

There are several problems here. I need to delegate to a static method, because I cannot get the value of the members directly in the constructor's initializer list. This method needs to create a copy of every member (here it's just i) and initialize them separately, so that it can pass them to another constructor before using the copy constructor to finally construct the initial object. Also, it takes a lot more lines of code because of the new constructor and the static method.

So, this approach seems doomed. Then I realized, what I really want is every instance of that class/struct to be const. But, as far as I know, there is no way to force a user to use the const keyword every time. So, I thought about using alias declarations. A bit like what the standard library does for const_reference and such (in pretty much every container). Only in this case, it would be the other way around : the type would be called NonConstType, or let's say MutableType, and the alias would be declared like so :

using Type = const MutableType;

And since I don't want to pollute the namespace, let's use a Mutable namespace. Here is what the code looks like :

namespace Mutable {
    struct C {
        C() { std::cin >> i; }

        int i;
    };
}

using C = const Mutable::C;

This way, I can provide an "immutable" class that handles like a C struct (without getters) but can still be constructed with data coming from different files. Also, the mutable version is still available, which I think might be a good thing after all.

So, is there another way ? Are there benefits or drawbacks I didn't think about, in any of these three codes ?

A full testing code can be found here.

Upvotes: 6

Views: 1202

Answers (8)

define cindy const
define cindy const

Reputation: 632

You mentioned forcing const-ness for your type as a solution, but not being sure how to force the client to construct your object as const. You could use a factory to achieve this goal:

struct MyType {
 public:
  static const MyType FromStream(std::istream &is) {
    return MyType(is);  // Most likely optimized as a move through copy elision 
  }
  static const MyType *MakeNewFromStream(std::istream &is) {
    return new MyType(is);
  }

  int a, b;
 private:
  MyType(std::istream &is) {
    is >> a >> b;
  }
};

And in use:

const MyType mt = MyType::FromStream(std::cin);
const MyType mt_ptr = MyType::MakeNewFromStream(std::cin);

Upvotes: 0

Cheers and hth. - Alf
Cheers and hth. - Alf

Reputation: 145359

Degrees of immutability.

Re

My current goal is to read data from a file to construct an object so that it cannot be modified subsequently

there are degrees of immutability, such as:

  • Totally immutable.
    That's the good old const, either for the type or for individual data members. Drawback: can't be moved, so, for example, it forces copying when used as function return value. However, the compiler may optimize away such copying, and will usually do so.

  • Immutable but movable.
    This allows efficient function return values even when the compiler doesn't optimize. Also great for passing an original temporary down a by-value call chain where the bottom function stores a copy: it can be moved all the way.

  • Immutable but movable and copy assignable.
    Assignable may not sound as being in the same design direction as immutable, and indeed a novice may think that these attributes are in direct conflict!, but e.g. Python and Java strings are examples of this: they're immutable, but assignable. Essentially this is a neat way to encapsulate a handle-value approach. User code deals with handles, but it appears to be dealing directly with values, and if user code could change a value, then some other part of the user code holding a handle to the same value would see the change, which would be ungood in the same way as a global variable's unexpected changes. Hence the values need to be immutable, but not the user code objects (which can be just handles).

The last point shows that there's a logical design level need to distinguish internal values from user code objects.

With this point of view the first point above is about both values and objects being immutable; the second point has immutable values and generally immutable objects, but allows efficient and delightfully low level pilfering of values from temporary objects, leaving them logically empty; and the third point has immutable values but objects that are mutable with respect to both copy assignment and moving from temporaries.

Data.

For all three possibilities we can define a simple internal Data class like this:

Data.hpp:
#pragma once
#include "cppx.hpp"     // cppx::String, an alias for std::wstring

namespace my {
    using cppx::String;

    struct Data
    {
        String  name;
        int     birthyear;
    };
}  // namespace my

Here cppx.hpp is a little helper file with ¹general convenience functionality, that I list at the end.

In your actual use case(s) the data class will probably have other data fields, but the main idea is that it's a simple aggregate class, just data. You can think of it as corresponding to the “value” in a handle-value approach. Next, let's define a class to use as the type of user code variables.

Totally immutable objects.

The following class implements the idea of user code objects that are totally immutable: the value set at initialization can't be changed at all, and persists until the object's destruction.

Person.all_const.hpp:
#include "Data.hpp"     // my::(String, Data), cppx::*

namespace my {
    using cppx::int_from;
    using cppx::line_from;
    using cppx::In_stream;      // alias std::wistream

    class Person
    {
    private:
        Data const  data_;

    public:
        auto operator->() const noexcept
            -> Data const*
        { return &data_; }

        explicit Person( In_stream& stream )
        try
            : data_{ line_from( stream ), int_from( stream ) }
        {} CPPX_RETHROW_X
    };
}   // namespace my

Here

  • the const for the data_ member provides the required total immutability;

  • the operator-> gives easy access to the Data fields;

  • the noexcept on operator-> may possibly help the compiler in some ways, but is mostly for the benefit of the programmer, namely documenting that this accessor doesn't throw;

  • the constructor is explicit because at the design level it does not provide a conversion from the stream argument;

  • the order of the calls to line_from and int_from, and hence the order of consumption of lines from the stream, is guaranteed ²because this is curly braces initializer list;

  • the line_from and int_from function are <cppx.hpp> helpers that each read one line from the specified stream and attempt to return respectively the complete line string, and the int produced by std::stoi, throwing an exception on failure; and

  • the CPPX_RETHROW_X macro picks up the function name and retrows the exception with that name prepended to the exception message, as a primitive explicit way to get a simple call stack trace in the exception.

Instead of operator-> one could have defined an accessor method called data, say, returning a Data const&, but operator-> gives a very nice usage syntax, as exemplified below:

An example main program.

main.cpp:
#include PERSON_HPP         // E.g. "Person.all_const.hpp"
#include <iostream>
using namespace std;

auto person_from( cppx::In_stream& stream )
    -> my::Person
{ return my::Person{ stream }; }

void cppmain()
{
    auto x = person_from( wcin );   // Will not be moved with the const version.
    wcout << x->name << " (born " << x->birthyear << ").\n";

    // Note: due to the small buffer optimization a short string may not be moved,
    // but instead just copied, even if the machinery for moving is there.
    auto const x_ptr = x->name.data();
    auto y = move( x );
    bool const was_moved = (y->name.data() == x_ptr);
    wcout << "An instance was " << (was_moved? "" : "not ") << "moved.\n";
}

auto main() -> int { return cppx::mainfunc( cppmain ); }

Here cppx::mainfunc, again a helper from <cppx.hpp>, takes care of catching an exception and displaying its message on the std::wcerr stream.

I use wide streams because that's the easiest way to support international characters for Windows console programs, and they also work in Unix-land (at least when one includes a call to setlocale, which is also done by cppx::mainfunc), so they're effecively the most portable option: they make this example most portable. :)

The code at the end doesn't make much sense for the totally immutable const version, so let's look at movable version:

Immutable but movable objects.

Person.movable.hpp
#include "Data.hpp"     // my::(String, Data), cppx::*
#include <utility>      // std::move

namespace my {
    using cppx::In_stream;
    using cppx::int_from;
    using cppx::line_from;
    using std::move;

    class Person
    {
    private:
        Data data_;

        auto operator=( Person const& ) = delete;
        auto operator=( Person&& ) = delete;

    public:
        auto operator->() const noexcept
            -> Data const*
        { return &data_; }

        explicit Person( In_stream& stream )
        try
            : data_{ line_from( stream ), int_from( stream ) }
        {} CPPX_RETHROW_X

        Person( Person&& other ) noexcept
            : data_{ move( other.data_ ) }
        {}
    };
}   // namespace my

Note that a move constructor needs to be explicitly specified, as shown at the end above.

As g++ explains it, if one doesn't do that then

my::Person::Person(const my::Person&)' is implicitly declared as deleted because 'my::Person' declares a move constructor or move assignment operator

Immutable but movable and assignable objects (a loophole!).

To make the objects assignable one can simply remove the = delete declarations.

But with this the automatic move constructor is not implicitly deleted, so the explicit version of it can be removed, yielding

Person.assignable.hpp:
#pragma once
#include "Data.hpp"     // my::(String, Data), cppx::*
#include <utility>      // std::move

namespace my {
    using cppx::In_stream;
    using cppx::int_from;
    using cppx::line_from;

    class Person
    {
    private:
        Data data_;

    public:
        auto operator->() const noexcept
            -> Data const*
        { return &data_; }

        explicit Person( In_stream& stream )
        try
            : data_{ line_from( stream ), int_from( stream ) }
        {} CPPX_RETHROW_X
    };
}   // namespace my

This is shorter and simpler, which is good.

However, since it supports copy assignment it allows a modification of a part of the value of an instance x.

How? Well, one way is by copying the complete Data value out of x, modifying that Data instance, formatting a corresponding string with the values on two lines, using that to initialize a std::wistringstream, passing that stream to the Person constructor, and assigning that instance back to x. Phew! What a roundabout hack! But it shows that it's possible, in theory, and rather inefficiently, to write e.g. a set_birthyear function for the copy assignable Person class. And such loopholes, sort of security holes in the type, sometimes create problems.

Still, I'm only mentioning that loophole for completeness, so that one can be aware of it – and perhaps become aware of similar functionality loopholes in other code. And I think that I would personally choose this version of the Person class. For the simpler it is, the easier it is to use and maintain.

For completeness: the cppx support used above.

cppx.hpp
#pragma once

#include <iostream>     // std::(wcerr, wistream)
#include <locale.h>     // setlocale, LC_ALL
#include <stdexcept>    // std::runtime_error
#include <string>       // std::(wstring, stoi)
#include <stdlib.h>     // EXIT_...

#ifndef CPPX_QUALIFIED_FUNCNAME
#   if defined( _MSC_VER )
#       define CPPX_QUALIFIED_FUNCNAME  __FUNCTION__
#   elif defined( __GNUC__ )
#       define CPPX_QUALIFIED_FUNCNAME  __PRETTY_FUNCTION__     // Includes signature.
#   else
#       define CPPX_QUALIFIED_FUNCNAME  __func__    // Unqualified but portable C++11.
#   endif
#endif

// Poor man's version, roughly O(n^2) in the number of stack frames unwinded.
#define CPPX_RETHROW_X \
    catch( std::exception const& x ) \
    { \
        cppx::fail( \
            cppx::Byte_string() + CPPX_QUALIFIED_FUNCNAME + " | " + x.what() \
            ); \
    }

namespace cppx {
    using std::endl;
    using std::exception;
    using std::runtime_error;
    using std::stoi;

    using String = std::wstring;
    using Byte_string = std::string;

    using In_stream     = std::wistream;
    using Out_stream    = std::wostream;

    struct Sys
    {
        In_stream& in       = std::wcin;
        Out_stream& out     = std::wcout;
        Out_stream& err     = std::wcerr;
    };

    Sys const sys = {};

    [[noreturn]]
    inline auto fail( Byte_string const& s )
        -> bool
    { throw runtime_error( s ); }

    inline auto line_from( In_stream& stream )
        -> String
    try
    {
        String result;
        getline( stream, result ) || fail( "getline" );
        return result;
    } CPPX_RETHROW_X

    inline auto int_from( In_stream& stream )
        -> int
    try
    {
        return stoi( line_from( stream ) );
    } CPPX_RETHROW_X

    inline auto mainfunc( void (&f)() )
        -> int
    {
        setlocale( LC_ALL, "" );    // E.g. for Unixland wide streams.
        try
        {
            f();
            return EXIT_SUCCESS;
        }
        catch( exception const& x )
        {
            sys.err << "! " << x.what() << endl;
        }
        return EXIT_FAILURE;
    }
}  // namespace cppx

¹ I think it would be nice if the Stack Overflow C++ community could standardize on such a file, to reduce the cognitive burden of reading examples in answers, and possibly in questions too!, but I think most readers will find my (and anyone else's) helpers pretty alien at first sight, and secondly I'm just too lazy to bring this idea over to the C++ Lounge and discuss it there, which IMO would be the way to do it.
² See (Order of evaluation of elements in list-initialization).

Upvotes: 1

Oliv
Oliv

Reputation: 18081

What you are looking for is member initializer list and return type optimization. The member initializer list is where one shall intialize non static const members:

struct A{
   A():i{some_function()}{}
   const int i;
}

The member initializer list is introduced by the ":" after the constructor declaration and before the constructor definition: :i{some_function}.

The somme_function can be any callable. In order to keep it optimal, enable return type optimization. There will be no copy of the return value to the member i. For example:

int some_function(){
  int a;
  cin >> a;
  return a;
}

The variable a is constructed in the context of the caller: the variable a "refers" to the member i. There will be no copy. The above code is optimal, it writes your file directly in the member i.

The declaration:

auto a = A();

is equivalent, in term of code generated, to:

cin >> a.i;

To enable enable this optimization, declare the return value inside the body of your function and return it by value.

And if one want more than one variable, the solution is anonymous union (which is standard) and anonymous structures (not standard):

struct A{
  struct data_structure {
    int i;
    int j;
    };
  struct raw_data_t{
    unsigned char data[sizeof(data_structure)];
  };
  union{
    const raw_data_t raw_data;
    struct{
    const int i;
    const int j;
    };
  };
  A():raw_data{some_function()}{}
  raw_data_t some_function(){
    raw_data_t raw_data;
    auto data = new(&raw_data) data_structure();
    cin >> data->i;
    cin >> data->j;
    return raw_data;
  }
};

This is much less sexy! And not standard, so not portable.

So let's wait C++17 operator.() overload!!

Upvotes: 0

max66
max66

Reputation: 66240

If I undestand well, you need to inizialize all data of you class with a single call to a single function.

I suppose you could wrap your class around another data class; by example, suppose you need an int, a long and a std::string, you could do something like

#include <iostream>

struct B3_Data {
   const int i;
   const long j;
   const std::string k;
};

struct B3 {

   using  dataType = B3_Data;

   const dataType  data = fromFile();

private:
   static dataType fromFile () {
      int i;
      long j;
      std::string k;
      std::cin >> i >> j >> k;
      return dataType {i, j, k};
    }
};

int main ()
 {
   B3 a;

   //a.data.i = 5; // error: data.i and i are const

   return 0;
 }

If you have not much data, you can pack all of they in a std::tuple; something like

#include <tuple>
#include <iostream>

struct B4 {

   using  dataType = std::tuple<int, long, std::string>;

   const dataType  data { fromFile() };

private:
   static dataType fromFile () {
      int i;
      long j;
      std::string k;
      std::cin >> i >> j >> k;
      return std::make_tuple(i, j, k);
    }
};


int main ()
 {
   B4 a;

   std::cout << std::get<0>(a.data) << std::endl;  // ok: reading

   // std::get<0>(a.data) = 5;                        // error: data is const

   return 0;
 }

Upvotes: 0

max66
max66

Reputation: 66240

You're using C++11 so ... why don't use default initialization?

A B derived solution

#include <iostream>

struct B2 {
    const int i { fromFile("filename1") };
    const int j { fromFile("filename2") };
    const int k { fromFile("filename3") };

private:
    static int fromFile (const std::string &) {
        int i;
        std::cin >> i;
        return i;
    }
};

int main ()
 {
   B2 a;

   // a.i = 5; // error: i is const
   // a.j = 7; // error: j is const
   // a.k = 9; // error: k is const

   return 0;
 }

Upvotes: 0

bipll
bipll

Reputation: 11940

My turn. Plain C++03, no aggregate.

struct B {
    B(): i_(read()), j_(read()) {}
    int i() { return i_; }
    int j() { return j_; }
private:
    int read() { int retVal; cin >> retVal; return retVal; }
    const int i_, j_;
};

If it were for only one attribute, things could be even simplier:

struct B {
    B();
    operator int() { return i; }
private:
    const int i;
};

Upvotes: 0

David Hammen
David Hammen

Reputation: 33126

You're using C++11, so why don't you use aggregate initialization?

#include <iostream>

struct Foo {
    const int val; // Intentionally uninitialized.
};

struct Foo create_foo_from_stream (std::istream& stream) {
    int val;
    stream >> val;
    return Foo{val};
}

int main () {
    Foo foo (create_foo_from_stream(std::cin));
    std::cout << foo.val << '\n';
}

The only way to initialize struct Foo is via aggregate initialization or copy construction. The default constructor is implicitly deleted.

Note that in C++14, you can use a default member initializer and still use aggregate initialization:

#include <iostream>

struct Foo {
    const int val = 0;  // Prevents aggregate in C++11, but not in C++14.
};

struct Foo create_foo_from_stream (std::istream& stream) {
    int val;
    stream >> val;
    return Foo{val};
}

int main () {
    Foo foo (create_foo_from_stream(std::cin));
    Foo bar;   // Valid in C++14.
    std::cout << foo.val << bar.val << '\n';
}

Upvotes: 2

Mark B
Mark B

Reputation: 96281

What about just using a template read helper? You don't need it to be static or a class member, a free template is all you need to extract the right values from the stream.

#include <iostream>

template <typename T>
T stream_read(std::istream& is)
{
    T val;
    is >> val;

    return val;
}

struct B
{
    B() : i_(stream_read<int>(std::cin)) { }

    const int i_;
};

int main()
{
    B b;

    std::cout << "value=" << b.i_ << std::endl;
}

Upvotes: 2

Related Questions