6

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.

Nelfeal
  • 12,593
  • 1
  • 20
  • 39
  • The `const` type can't be moved, only copied. – Cheers and hth. - Alf Jun 09 '16 at 13:54
  • `C` is what you want, except do the reading in a different function and pass final parameters to the constructor. And it would be more readable to not use the `using C`, write `const` each time you declare a const object. – M.M Jun 09 '16 at 13:56
  • @M.M. The `C` type is immutable, but the OP states that forced copying is undesirable, and the `C` type forces that. – Cheers and hth. - Alf Jun 09 '16 at 13:58
  • @Cheersandhth.-Alf I don't see anything about "forced copying" in the post (whatever that is supposed to mean). OP stated that the type should be immutable and I take that to mean that it is `const` once it has been constructed (with the intentional consequence that it cannot be moved from). – M.M Jun 09 '16 at 14:09
  • @M.M. When you return a `const` type object from a function, it can not be moved. So the `const` forces copying. That copying can in some cases be elided by return value optimization. Likewise when you pass such an object by value, in particular through two or more levels of calls, except that for this the copying can't be optimized away except by very aggressive inlining. You won't find the words "forced copying" in the post because (1) it is a **logical consequence**, and (2) the OP is not necessarily aware of that. – Cheers and hth. - Alf Jun 09 '16 at 15:59
  • @M.M. Regarding what the OP means by "immutable", my ESP circuits fail me. Sorry. But it's not `const`, since the first example doesn't have that keyword. I think it's more on the line of a Python/Java immutable string, which can't be modified but can be copy assigned in whole. – Cheers and hth. - Alf Jun 09 '16 at 16:04
  • @M.M What do you mean by "final parameters" ? Were you thinking about David Hammen's solution ? Also, the point of the `using` alias declaration is to provide a `const` by default. So, the user shouldn't be typing `const`, but rather should be typing something more if the mutable version is wanted (in this case `Mutable::C` instead of just `C`). – Nelfeal Jun 09 '16 at 16:08
  • @Cheersandhth.-Alf I wasn't fully aware of this `const` drawback. But in my case, I don't think it is important because objects of this type won't be passed around too often. It is worth knowing, though. By "immutable", I effectively mean "not modifiable". And the `C` type is, I think, the closest one to what I mean. Ideally, it would be copyable and movable too, but I guess it would be asking for too much. Would a `B` kind of type be movable ? – Nelfeal Jun 09 '16 at 16:16
  • @Cheersandhth.-Alf "copying" a Java string actually copies references to a string and leaves the string content unchanged (a better analogy would be copying `std::shared_ptr`). Re. the return value issue, obviously return values would not be const-qualified. – M.M Jun 09 '16 at 22:19
  • @Nelxiost Forget what I said about final parameters, sorry. C++ defaults to non-const, IMO it will make your code harder to understand if you try and make a type with unusual sematics. An analogy might be overloading `operator*` to do addition. It's legal but people won't expect it. As a library author you want to help your users, not confuse or force them, and following normal conventions is a good way to help them. – M.M Jun 09 '16 at 22:23
  • @M.M. Thank you for your attempts to clarify. – Cheers and hth. - Alf Jun 10 '16 at 19:48

8 Answers8

2

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;
}
Mark B
  • 95,107
  • 10
  • 109
  • 188
  • I find the template interesting, but as I said in my comment to max66's answer, it looks too restrictive. Considering I would need to initialize multiple data members from the same file, I would need to add extra parameters to that function, and it would prevent me from doing other things while reading the file (conditional search for exemple). – Nelfeal Jun 09 '16 at 16:38
  • @Nelxiost Can you elaborate on why you need extra parameters to the function? I set it up specifically so you can extract as many or few pieces of data from the file as you like. If you need to do other things while reading the file you should clearly state that in your question. – Mark B Jun 09 '16 at 16:50
  • Consider a file that could contain "1 2 3" or "1 1 3". The struct has two `int` members, a and b. How do I use your method to get 1 into a and, in the first case, 2 into b, but in the second case, 3 into b (discarding the 1) ? – Nelfeal Jun 09 '16 at 16:56
  • @Nelxiost what are the rules for whether to accept or reject a value from the file? – Mark B Jun 09 '16 at 17:02
  • I'll reformulate. If the file contains "1 2 3", then I want to put the first value *1* into a, and the second value *2* into b. If the file contains "1 1 3", then I still want to put the first value *1* into a, but I want to discard the second *1* and put the third value, *3*, into b. That would be an exemple where I want to discard duplicates. What I currently have, though, is a file formatted like a dictionnary, with keys and values. In this second case, the extra parameter would be the key to look for. – Nelfeal Jun 09 '16 at 17:18
  • As I explain in a comment on David Hammen's answer, I ended up using mostly his solution, but I also got inspired by your way of using a template. So, have a upvote. – Nelfeal Jun 11 '16 at 23:01
2

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';
}
David Hammen
  • 32,454
  • 9
  • 60
  • 108
  • Isn't public const member subject to `const_cast`? – bipll Jun 09 '16 at 15:49
  • I feel somewhat uneasy about calling another function than the constructor when instantiating the type. Which is why I tried, in my `B` struct, to only provide the constructor. But I guess this solution is a good one after all. Would you recommend it over the last one I mentionned (the `C` struct) ? – Nelfeal Jun 09 '16 at 16:03
  • @Nelxiost -- A class that has a user-provided constructor is not an [aggregate](http://en.cppreference.com/w/cpp/language/aggregate_initialization). The only thing that keeps your struct `C` from being an aggregate is the constructor. The only way to initialize such classes is by invoking one of the defined constructors or a non-deleted copy or move constructor. Aggregate initialization can be quite handy. – David Hammen Jun 09 '16 at 16:21
  • @bipll -- "Don't do that then!" The same applies to the classes defined in the question. The nice thing about `const_cast` is that its use is searchable -- and rightfully questioned during code review. C-style casts are a bit harder to root out by a human review, but compilers are quite good at finding them. – David Hammen Jun 09 '16 at 16:24
  • Well, I ended up using some mix between your solution and Mark B's. Although, I am marking your answer because of the way I read the file inside a separate function. – Nelfeal Jun 11 '16 at 22:58
1

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).

Community
  • 1
  • 1
Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
  • Thank you for the dedication! Interesting stuff. However, I picked a simpler solution that I think was more fitting in my code. – Nelfeal Jun 11 '16 at 22:54
  • @Nelxiost: The main differences between the answer A you picked as solution, and the totally immutable object above, is (1) A uses a factory function instead of a constructor, [contrary to your wishes](http://stackoverflow.com/questions/37727867/immutable-struct-with-non-trivial-constructor/37756070?noredirect=1#comment62934497_37728873); (2) A does no error checking, and (3) A doesn't factor out the data representation. Reading now the commentary following the linked one, I see that there are some unstated and very unclear requirements about parsing a text file. Sorry for not seeing that. – Cheers and hth. - Alf Jun 11 '16 at 23:18
  • Well, the answers made me realize that what I truly needed was impossible or very impractical to code. So I settled on some sort of factory function, except I used an intermediate so that my class doesn't deal with a file (or a stream), but rather with formatted data, extracted from the file beforehand. That way, I still use the constructor of my class, but I don't directly give it the file. Maybe I haven't described the problem quite enough, sorry for that. Should I edit my question (and specify my final solution) ? Also, why would I want to factor out the data representation of my class ? – Nelfeal Jun 11 '16 at 23:45
0

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;
 }
max66
  • 65,235
  • 10
  • 71
  • 111
  • Sorry if that wasn't clear in my post, but the struct would, in practice, have more than one attribute. And I don't see how to extend your solution to multiple attributes. – Nelfeal Jun 09 '16 at 15:49
  • @Nelxiost - with "attributes" do you mean "members"? In this case, yes: it was clear; but I don't see the problem. I've modified my answer for initialize 3 const members – max66 Jun 09 '16 at 16:06
  • Oh, I see. Suddenly, it looks a bit like Mark B's answer. I prefer this style, with aggregate initialization, but I would need a template to read different types of values. However, the problem I have with this is the implementation of `fromFile`. It seems that I won't be able to do much else than read the first values from different files. I guess I could add parameters to `fromFile` in order to read a certain line or to find a key (if the file is formatted like a dictionnary). My `B` type isn't as restrictive. – Nelfeal Jun 09 '16 at 16:34
  • Oh, and by "attributes", I do mean data members. I just realized it was used for a completely unrelated thing, so I replaced it in my question. – Nelfeal Jun 09 '16 at 16:46
  • What I have at the moment is a single call to `fromFile` that initializes every data member by following specific rules. The thing is, in your current code and considering you open a file with the parameter instead of using `std::cin`, you can only get the first value out of (different or not) files. – Nelfeal Jun 09 '16 at 17:12
  • @Nelxiost - yes, my solution is an evolution of your B solution; but I don't understand what are your desiderata. Do you mean that a single call of your function (`fromFile()` like) should initialize all members? – max66 Jun 09 '16 at 17:27
  • @Nelxiost - sorry: I've deleted my comment because where "methods" instead of "members" and I haven't seen your comment – max66 Jun 09 '16 at 17:30
  • @Nelxiost - I see; I think that now I understand your problem (I hope); and I'm thinking two different solutions (different... well, not really) but I suppose you should make clear this in your question – max66 Jun 09 '16 at 17:34
  • @Nelxiost - I've written another answer because it's really different (but ever an evolution of your mark B) – max66 Jun 09 '16 at 18:07
0

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;
};
bipll
  • 11,747
  • 1
  • 18
  • 32
  • Isn't that a convoluted version of my `A` class ? – Nelfeal Jun 09 '16 at 15:57
  • Thanks, edited. Additionally, this one is implicitly coerced to int. – bipll Jun 09 '16 at 16:00
  • Sorry, I was talking about your first code. It seems to me like you just displaced the problem by making the attributes `const` and delegating their construction to `read()`. But, since the attributes are already private, I don't see the benefit of `const` here. – Nelfeal Jun 09 '16 at 16:26
  • The OP specifically didn't like the idea of getters. Presumably @Nelxiost's class contains a good deal more data members than the two shown in this answer. Aside: private data is [hackable](http://www.gotw.ca/gotw/076.htm). The answer is to exposing private data or casting away constness is "Don't do that then!" – David Hammen Jun 09 '16 at 16:38
  • @DavidHammen `#define private public` is my favorite now. :D – bipll Jun 09 '16 at 17:06
  • @Nelxiost might be a hint to the compiler, otherwise of no use. – bipll Jun 09 '16 at 17:09
  • @DavidHammen Thank you for the good read. @bipll In that case, to me, it's just another version of `A`, but a bit more difficult to read... – Nelfeal Jun 09 '16 at 17:19
0

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!!

Oliv
  • 17,610
  • 1
  • 29
  • 72
  • Thank you, but this is what I've done with `B`, except here, it doesn't work with multiple data members. And I know about the return value optimization, but it is off-topic. – Nelfeal Jun 09 '16 at 17:26
  • OK, you right, you need an anonymous union and non standard anonymous structure! I thought you rejected B because you did not knew about this optimization. – Oliv Jun 09 '16 at 17:41
0

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;
 }
max66
  • 65,235
  • 10
  • 71
  • 111
0

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