Experiment: functional concepts in C

  • Thread starter Ertugrul Söylemez
  • Start date
R

Robert Latest

["Followup-To:" header set to comp.lang.c.]
jacob said:
lcc-win proposes a garbage collector in its standard distribution.
All the problems above are solved with a gc

Why insist on "lcc-win" when the garbage collector can be linked into
any C program on any hosted implementation?

robert
 
J

jacob navia

Robert Latest a écrit :
["Followup-To:" header set to comp.lang.c.]
jacob said:
lcc-win proposes a garbage collector in its standard distribution.
All the problems above are solved with a gc

Why insist on "lcc-win" when the garbage collector can be linked into
any C program on any hosted implementation?

robert

Because I am the author of that software. I recompiled the work of
Mr Boehm's with lcc-win in 2003 and added it to the standard distribution in 2004.

As far as I know, it was the only C compiler that included a GC in the
standard distribution at that time. I have been promoting the use of a
GC in this group since several years, since I consider it a very good solution
(but not a panacea) in many situations, for instance in this case: functional
style programming.

There are maybe other ports of the collector under windows, but they
are tied to cygwin, as far as I know. The collector compiled with lcc-win
can be used (since it is a dll) with other runtimes, watcom/microsoft, whatever.

Those are some of the reasons I mention my work here.
 
J

jacob navia

Robert Latest a écrit :
And your point is...?

If you need a certain functionality in a C program, you have several
options:

1) Link against a third-party library that implements what you need
2) Implement it yourself
3) Switch to a different language that has the feature built-in
4) Invent a C-like language that has the desired features and write
a full-fledged implementation
5) Convince the Standard Committee to have the desired functionality
in the next release of the C Standard

You seem to be hell-bent on options 4) and 5) while ordinary mortals are
perfectly happy with 1) thru 3).

So again, what's your point?

robert

As you have perfectly seen, Mr "Latest", I am following options (4) and (5).

I am not an ordinary mortal?

I do not think so.

I just suspect that you do not like my work, that's all.
 
I

Ian Collins

Michael said:
In general, metaprogramming is orders of magnitude harder to achieve
in C++ than C, because of severely bloated language constructs and
rules.

Care to expand on that?
 
I

Ian Collins

jacob said:
Robert Latest a écrit :
["Followup-To:" header set to comp.lang.c.]
jacob said:
lcc-win proposes a garbage collector in its standard distribution.
All the problems above are solved with a gc

Why insist on "lcc-win" when the garbage collector can be linked into
any C program on any hosted implementation?

Because I am the author of that software. I recompiled the work of
Mr Boehm's with lcc-win in 2003 and added it to the standard
distribution in 2004.

As far as I know, it was the only C compiler that included a GC in the
standard distribution at that time.

Sun's compiler collection has had libgc since the mid-late 90s.
 
J

jacob navia

Ian Collins a écrit :
jacob said:
Robert Latest a écrit :
["Followup-To:" header set to comp.lang.c.]
jacob navia wrote:

lcc-win proposes a garbage collector in its standard distribution.
All the problems above are solved with a gc

Why insist on "lcc-win" when the garbage collector can be linked into
any C program on any hosted implementation?

Because I am the author of that software. I recompiled the work of
Mr Boehm's with lcc-win in 2003 and added it to the standard
distribution in 2004.

As far as I know, it was the only C compiler that included a GC in the
standard distribution at that time.

Sun's compiler collection has had libgc since the mid-late 90s.

Thanks. I wasn't aware of that.
 
E

Ersek, Laszlo

The problem is not garbage collection. It just makes it even more
complicated. C is missing some very important high level control
constructs, most notably closures. The only functional programming you
can do in C is continuation passing style, which is already an
improvement over the usual purely imperative style. You get some
limited, but safe resource management that way.

I have written an article about that, which you can find here:
<http://blog.ertes.de/2009/02/continuations-for-secure-code.html>.

(Since you've mentioned the article here and for me it appears on-topic,
I'll respond here. If you don't mind, I'll quote the function.)

----v----
int withFile(const char *fileName,
const char *mode,
int (*user)(FILE *))
{
FILE *fh;
int err;

fh = fopen(fileName, mode);
if (fh == NULL) return 1;

err = user(fh);
fclose(fh);
return err;
}
----^----

You don't check the return value of fclose(). If the last operation in
(*user)(fh) was a write that remained buffered (_IOFBF/_IOLBF), then the
current fclose() invocation can lead to silent data loss. In my view,
either the return value of fclose() should be checked in withFile(), or
the user callback should be made explicitly responsible to close up shop
with an fflush().

Perhaps you've omitted the check for simplicity's sake, but -- if I
understand it correctly -- the point of the article is exactly to move
the responsibility of closing file handles (and to do so correctly) from
the programmer to the RTS (run-time system).

If, indeed, the run-time system cannot "talk back" to the programmer by
design, then there's no way around flushing the stdio stream within
(*user)(fh). In that vein, if a program (in general) ever wrote to
stdout, it should explicitly flush it before exiting with EXIT_SUCCESS,
so that at least the return status can be changed to EXIT_FAILURE or
some such in the last minute.

[not standard C] This approach is obviously not sufficient for
hardware-level data safety (many times not even fsync() suffices for
that, allegedly), but it does warn about "disk full", "maximum file size
exceeded" and similar software conditions.

Digressing even more off topic,

(1) The same holds for manipulating (usually regular) files through file
descriptors: NFS servers tend to buffer up writes until the final
close(). At least a checked close(STDOUT_FILENO) is necessary in filter
programs if the process is about to exit successfully.

(2) This is why putting close / flush operations and the like in C++
destructors qualifies as a bad idea. Destructors must not throw any
exceptions and are unable to return values, so there's no easy way for
them to communicate failure back to the programmer. I guess they could
modify some non-member objects, but destructors exist so that the
programmer can largely forget about manually destroying objects.

(3) Garbage collection (in Java) usually delays the release of the
memory allocated for the object and the execution of the corresponding
destructor (finalizer) until the garbage collector sees fit.
Unfortunately, this can delay the release of other (remarkably
non-memory) resources; thus the garbage collector holds back types of
resources it has nothing to do with. The programmer best calls some
close() method manually, completely defeating the purpose of garbage
collection and exceptions.

http://stackoverflow.com/questions/171952/is-there-a-destructor-for-java/171957#171957

-o-

If I deliberately ignore the scalar value returned by a function call, I
like to write

(void)fun(...);

as shown in C90 6.6.3 "Expression and null statements" (or C99 6.8.3
"Expression and null statements"), Example 1:

----v----
If a function call is evaluated as an expression statement for its side
effects only, the discarding of its value may be made explicit by
converting the expression to a void expression by means of a cast:

int p(int);
/*...*/
(void)p(0);
----^----


Cheers,
lacos
 
L

ld

The problem is not garbage collection.  It just makes it even more
complicated.  C is missing some very important high level control
constructs, most notably closures.  The only functional programming you
can do in C is continuation passing style, which is already an
improvement over the usual purely imperative style.  You get some
limited, but safe resource management that way.

I have written an article about that, which you can find here:
<http://blog.ertes.de/2009/02/continuations-for-secure-code.html>.

That article builds on the just-for-fun article, which started this
thread, and turns it into something actually useful.

you should remove "This is impossible in C" from the last line of your
article since with my C lib, I use closures as I would in FP. For
example:

Haskell:
fmap (\x -> fmap (\y -> fmap (\z -> fun3 x y z) zs) ys) xs

C Object System:
gmap(gmap(gmap(fun3(aLzy2(__1), aLzy(__1), __1), zs), ys), xs)

The main difference is that Haskell closures have lexical scoping and
lazy evaluation while COS closures have dynamic scoping (placeholders)
and C has strict evaluation. This is why you need to delay the
evaluation of the closure' arguments to the proper evaluation
environment (e.g. geval) using the aLzy() qualifier:

aLzy2(__1) -> 1st argument of the outer map <=> x
aLzy (__1) -> 1st argument of the middle map <=> y
__1 -> 1st argument of the inner map <=> z

Dynamic scoping also means that you can build the closure outside the
lexical scope:

OBJ fun = fun3(aLzy2(__1), aLzy(__1), __1);
gmap(gmap(gmap(fun, zs), ys), xs);

is also valid (and impossible in Haskell AFAIK).

regards,

ld.
 
E

Ertugrul Söylemez

ld said:
you should remove "This is impossible in C" from the last line of your
article since with my C lib, I use closures as I would in FP. For
example:

Haskell:
fmap (\x -> fmap (\y -> fmap (\z -> fun3 x y z) zs) ys) xs

C Object System:
gmap(gmap(gmap(fun3(aLzy2(__1), aLzy(__1), __1), zs), ys), xs)

Of course you can always write an interpreter for a richer language.
That's not really 'doing it in C'. Just look at glib, which implements
a complete object oriented language on top of C. You're developing in
your higher language, which is expressed in terms of C.

The main difference is that Haskell closures have lexical scoping and
lazy evaluation while COS closures have dynamic scoping (placeholders)
and C has strict evaluation. This is why you need to delay the
evaluation of the closure' arguments to the proper evaluation
environment (e.g. geval) using the aLzy() qualifier:

aLzy2(__1) -> 1st argument of the outer map <=> x
aLzy (__1) -> 1st argument of the middle map <=> y
__1 -> 1st argument of the inner map <=> z

Dynamic scoping also means that you can build the closure outside the
lexical scope:

OBJ fun = fun3(aLzy2(__1), aLzy(__1), __1);
gmap(gmap(gmap(fun, zs), ys), xs);

is also valid (and impossible in Haskell AFAIK).

It's not impossible. Your closure can be a stateful computation:

myClosure = do
x <- gets var1
y <- gets var2
return (x+y)

In can also alter that surrounding state this way. Adding the
continuation monad it can even escape into its current context:

myClosure escape = do
x <- lift get
when (x == 2) (escape True)
return False

The 'escape' function may be a continuation, so the caller can instruct
this function to escape anywhere he wants, if the state happens to be 2.
I use this often to escape from 'forever' computations to a higher
context (which may even be unknown at that point).

I don't think that you can construct any control structure in C, which
can't be expressed easily in Haskell.


Greets
Ertugrul
 
E

Ertugrul Söylemez

Paul Rubin said:
Interrupt handler?

That's not a control structure. In fact it's rather an "out of control"
structure. How you handle interrupts depends on the context. If you're
using an operating system, you can use signal handlers or operating
system functions for that. Some interrupts are converted to exceptions
in many languages. There is no difference to C here, except that you
don't have exceptions in C and you can exploit Haskell's concurrency
system, which is also missing in C.

If instead you're _writing_ an operating system, it depends on what your
hardware abstractions look like. See House [1] for an example. So yes,
these things are well possible to do in Haskell.

[1] http://programatica.cs.pdx.edu/House/


Greets
Ertugrul
 
M

Michael Foukarakis

Care to expand on that?

I'll be as brief as I can:

- Implicit type conversions. How the following example compiles OK
without a warning is beyond me:
#include <iostream>
void foo(const std::string &) {}
int main()
{
foo(false);
}

- The "type name or object name?" issue is one of the little things
that can make parsing C++ a nightmare. You can get different parse
trees based on the output of sizeof, for instance. And sizeof is
dependent on the platform and implementation of inheritance, virtual
functions, etc.
- Current programming idioms (in C++, that is) and its lack of
reflection force us to use static binding to implement polymorphism.
So, for a common and seemingly simple application of metaprogramming,
such as printing objects at runtime, you'll get N layers of syntax and
meticulously placed overloads of operator<< in carefully #include'd
header files, and even in that case some compilers will generate
erroneous code without as much as a warning (that is, if they got past
the implicit type conversion hell).
- The conscious decision of not adding high-level types has led to,
most significantly, lack of easy-to-use containers. It has its
upsides, I agree. However, when you get around to use (and not
implement) ADTs, debugging can be a bitch. And when it comes to
implementation, at the heart of every high-level container you'll find
templates, relatively sophisticated ones too in case you're dealing
with the standard library and even more so with the Swiss army knife
that's called Boost. Everyone knows in what ways templates suck, I
won't list them.
- Duplicate facilities. For everything from printing to initializing
an object to using an array. They don't interoperate very well (check
out some code dealing with char* and std::string if you feel like
getting an aneurysm), and for that reason the C++ crowd shuns the use
of C syntax and features in C++ programs, although they still claim
compatibility with C is a good thing. I always chuckle when I see that
written. Heh.
- Manual memory management. A good thing. I support this 100%. It has
its place in the language and in today's systems. But it's
incompatible with templates and operator overloading. Can't have it
all (maybe in another language :).
- Inlining, although not as much related to metaprogramming as the
ones above. For this part, I'm quoting: "People define large functions
inline for two reasons. Some of them "care" (emotionally) about
performance, but never actually measure it, and someone told them that
inlining speeds things up, and forgot to tell them how it can slow
them down." Like when inline functions are implicitly called or
generated. Sometimes you'll have a huge class that needs to be
accessible globally. A singleton, I hear you saying! Nah, perhaps
other facilities of your program need to notify it, or modify it, and
others need to see the changes. Anyway that huge class will at least
get created and destroyed once. But that class will probably have no
constructor, no destructor, no operator= either - the compiler will
generate code for those anyway. And that may not be a problem until
you crash nearby and get a pile of assembly (which you haven't
written, it's not even your compiled code!) without any diagnostics
whatsoever.

I mean, those are just a few. I've seen literally thousands of
complaints about the language constructs, and sadly most came from
people who were way more knowledgeable than me on the issues at hand.
It's not that C++ is bad, it's very useful and rewards you when you're
not overzealous, but it's unsuitable for certain types of problems,
for which it's (sadly, again) used for very often. That's one of the
disadvantages of people calling it Object Oriented, without actually
being such in a satisfying degree, I guess. At least BS is less stuck
up than most people, when it comes to it.

Bah, sorry for the wall of text. I guess I couldn't help it. :)
 
N

Nick

Ertugrul Söylemez said:
The problem is not garbage collection. It just makes it even more
complicated. C is missing some very important high level control
constructs, most notably closures. The only functional programming you
can do in C is continuation passing style, which is already an
improvement over the usual purely imperative style. You get some
limited, but safe resource management that way.

I have written an article about that, which you can find here:
<http://blog.ertes.de/2009/02/continuations-for-secure-code.html>.

That article builds on the just-for-fun article, which started this
thread, and turns it into something actually useful.

Commenting there is so painful (the comment editor embedded in the page
doesn't recognise arrow keys!) I'll reply here instead!

It's an interesting approach, and I can certainly see its value in a
structure that requires a lot of specialised initialisation and
destruction. You could replace:

struct foo *foop = Create_Foo_Structure();
lots-and-lots-of-code-using foop
Release_Foo_Structure(foop)

with

withFoo(dofoopstuff);

but, of course, you have to create dofoopstuff(struct foo *foop) before
you can do that, and in that case writing:

struct foo *foop = Create_Foo_Structure();
dofoopstuff(foop);
Release_Foo_Structure(foop)

does the same job, and you aren't likely to forget to release it when
it's only two lines from creation.

And that's where I think this fails. If this is the only benefit you
get (in C obviously - I know some of the stuff you can do in a better
framework) then it doesn't seem to give anything more than a programming
discipline that says:
- remove anything that uses an allocated structure (including a file
pointer) into a separate function.
- call the function immediately between allocation and release of the
structures, and don't place /any/ other code in there.

Still a neat idea, but I'm not convinced it normally has any practical
value.
 
N

Nick Keighley

(Since you've mentioned the article here and for me it appears on-topic,
I'll respond here. If you don't mind, I'll quote the function.)

----v----
int withFile(const char *fileName,
             const char *mode,
             int (*user)(FILE *))
{
  FILE *fh;
  int err;

  fh = fopen(fileName, mode);
  if (fh == NULL) return 1;

  err = user(fh);
  fclose(fh);
  return err;}

----^----

You don't check the return value of fclose().

what do you do if fclose() fails? Admittedly withFile() shouldn't be
ignoring the fclose(), we might one to tell the user that his last
transaction hasn't gone into the master record!

If the last operation in
(*user)(fh) was a write that remained buffered (_IOFBF/_IOLBF), then the
current fclose() invocation can lead to silent data loss. In my view,
either the return value of fclose() should be checked in withFile(), or
the user callback should be made explicitly responsible to close up shop
with an fflush().

if fclose() doesn't work why should fflush()?

(2) This is why putting close / flush operations and the like in C++
destructors qualifies as a bad idea. Destructors must not throw any
exceptions and are unable to return values, so there's no easy way for
them to communicate failure back to the programmer. I guess they could
modify some non-member objects, but destructors exist so that the
programmer can largely forget about manually destroying objects.

but dtors become much less useful if they don't clean up. RAII doesn't
work any more.

-o-

If I deliberately ignore the scalar value returned by a function call, I
like to write

    (void)fun(...);

as shown in C90 6.6.3 "Expression and null statements" (or C99 6.8.3
"Expression and null statements"), Example 1:

----v----
If a function call is evaluated as an expression statement for its side
effects only, the discarding of its value may be made explicit by
converting the expression to a void expression by means of a cast:

    int p(int);
    /*...*/
    (void)p(0);
----^----

I think this is an ugly and clutterd coding style



--
"Reports that say that something hasn't happened are always
interesting to me,
because as we know, there are known knowns; there are things we know
we know.
We also know there are known unknowns; that is to say we know there
are some
things we do not know. But there are also unknown unknowns
-- the ones we don't know we don't know."
(Rumsfeld)
 
I

Ian Collins

Michael said:
I'll be as brief as I can:
- Current programming idioms (in C++, that is) and its lack of
reflection force us to use static binding to implement polymorphism.
So, for a common and seemingly simple application of metaprogramming,
such as printing objects at runtime, you'll get N layers of syntax and
meticulously placed overloads of operator<< in carefully #include'd
header files, and even in that case some compilers will generate
erroneous code without as much as a warning (that is, if they got past
the implicit type conversion hell).

How does this relate to a comparison of metaprogramming in C and C++?
- The conscious decision of not adding high-level types has led to,
most significantly, lack of easy-to-use containers. It has its
upsides, I agree. However, when you get around to use (and not
implement) ADTs, debugging can be a bitch. And when it comes to
implementation, at the heart of every high-level container you'll find
templates, relatively sophisticated ones too in case you're dealing
with the standard library and even more so with the Swiss army knife
that's called Boost. Everyone knows in what ways templates suck, I
won't list them.

Again, I don't see how this is harder than C, which doesn't have
high-level types either.
- Duplicate facilities. For everything from printing to initializing
an object to using an array. They don't interoperate very well (check
out some code dealing with char* and std::string if you feel like
getting an aneurysm), and for that reason the C++ crowd shuns the use
of C syntax and features in C++ programs, although they still claim
compatibility with C is a good thing. I always chuckle when I see that
written. Heh.

Do they? I've never come across that.
- Manual memory management. A good thing. I support this 100%. It has
its place in the language and in today's systems. But it's
incompatible with templates and operator overloading. Can't have it
all (maybe in another language :).

How?

I mean, those are just a few. I've seen literally thousands of
complaints about the language constructs, and sadly most came from
people who were way more knowledgeable than me on the issues at hand.
It's not that C++ is bad, it's very useful and rewards you when you're
not overzealous, but it's unsuitable for certain types of problems,
for which it's (sadly, again) used for very often. That's one of the
disadvantages of people calling it Object Oriented, without actually
being such in a satisfying degree, I guess. At least BS is less stuck
up than most people, when it comes to it.

Yet again, I don see how this relates to a comparison with C.
 
M

Michael Foukarakis

How does this relate to a comparison of metaprogramming in C and C++?

I assume you're aware of the definition of metaprogramming. In C++,
you have to deal with the use cases I described above. In C, you
don't, since the concept of polymorphism, overloading, templates are
not there. There's your extra complexity.
Again, I don't see how this is harder than C, which doesn't have
high-level types either.

Again, more complex language constructs to manage. I don't see how I
could've made this any simpler, sorry.
Do they? I've never come across that.

Well, maybe I've come across some particularly bad code then.

Here's a quick example:

Matrix add(const Matrix& x, const Matrix& y);
Matrix operator+(const Matrix& x, const Matrix& y);
Matrix use_add(const Matrix& a, const Matrix& b, const Matrix& c)
{
return add(a,add(b,c));
}
Matrix use_plus(const Matrix& a, const Matrix& b, const Matrix& c)
{
return a + b + c;
}

Now for the philosophical question: Where does The Matrix live?

- is it allocated by new and returned by reference? Who is supposed to
clean it up? Who manages the temporaries created by a+b+c ?
- is it returned by value? Won't the copy constructor copy lots of
matrices then? Not particularly pleasant when it comes to matrix
operations (which will be quite a lot if you're actually aiming for a
usable Matrix class - I assume I don't have to show you applications
of matrices as well).

Now consider what happens when overloaded operators may fail. There's
no good way to return an error, there are no context-free matrix "bad
values", because the whole point of operator+ is to be able to write (a
+b+c+d)..Go ahead, throw an exception. Throw it in a templated
constructor. Feel the madness. :)
Yet again, I don see how this relates to a comparison with C.

Have you tried any of the above in practice? Some non-trivial project
with more than a pair of developers? I'm sure that if you did, you'd
find it pretty daunting. Maybe not impossible to achieve, but not
without major compromises in your requirements.
 
I

Ian Collins

Michael said:
I assume you're aware of the definition of metaprogramming. In C++,
you have to deal with the use cases I described above. In C, you
don't, since the concept of polymorphism, overloading, templates are
not there. There's your extra complexity.

Ah, I see where the confusion lies, when you wrote "metaprogramming is
orders of magnitude harder to achieve in C++ than C" I assumed you were
referring to the metalanguage, rather than the object language. Neither
can be their own metalanguage.
Have you tried any of the above in practice? Some non-trivial project
with more than a pair of developers? I'm sure that if you did, you'd
find it pretty daunting. Maybe not impossible to achieve, but not
without major compromises in your requirements.

Oh I've worked on and managed large C++ projects and yes it can be
daunting, but also rewarding!
 
M

Michael Foukarakis

Ah, I see where the confusion lies, when you wrote "metaprogramming is
orders of magnitude harder to achieve in C++ than C" I assumed you were
referring to the metalanguage, rather than the object language.  Neither
can be their own metalanguage.


Oh I've worked on and managed large C++ projects and yes it can be
daunting, but also rewarding!

True. In cash, too. :p
 
L

ld

Of course you can always write an interpreter for a richer language.
That's not really 'doing it in C'.  Just look at glib, which implements
a complete object oriented language on top of C.  You're developing in
your higher language, which is expressed in terms of C.

Yes, that is a DSL but more general than usually done since its goal
is to extend the programming paradigms available in C. So for you, the
hundred of so DSL developed on top of Haskell are not Haskell?
Text.Printf is not Haskell? Yes, DSL are usually hackish, but there is
good advantage to stay with the same language.
It's not impossible.  Your closure can be a stateful computation:

  myClosure = do
    x <- gets var1
    y <- gets var2
    return (x+y)

But then you have to encapsulate fmap elements into the State monad
since you use is as a kind of placeholder...
In can also alter that surrounding state this way.  Adding the
continuation monad it can even escape into its current context:

  myClosure escape = do
    x <- lift get
    when (x == 2) (escape True)
    return False

The 'escape' function may be a continuation, so the caller can instruct
this function to escape anywhere he wants, if the state happens to be 2.
I use this often to escape from 'forever' computations to a higher
context (which may even be unknown at that point).

Yep, this is the way Haskell manage exception AFAIK.
I don't think that you can construct any control structure in C, which
can't be expressed easily in Haskell.

you mean something like:

defmethod(OBJ, ifThenElse, TrueFalse, Functor, Functor)
retmethod(_1 == True ? geval(_2) : geval(_3));
endmethod

The second expression (held by _3) remains unevaluated if the boolean
(_1) is False. Comparing to Haskell, you have to force laziness as in
the previous example to break strict evaluation of C. But this is also
true in almost all language except haskell (e.g. scheme).

regards,

ld.
 
G

gwowen

Here's a quick example:
Now for the philosophical question: Where does The Matrix live?
- is it allocated by new and returned by reference?

It behaves "as if" it were created as an "auto" and copied on
assignment.
Who is supposed to clean it up?

Whoever takes a copy cleans up their own copy.
Who manages the temporaries created by a+b+c ?

RAII. They're automatically destroyed once they're used.
- is it returned by value?

It is "as if" it is, yes.
Won't the copy constructor copy lots of matrices then?

Only if the compiler cannot optimise the assignment away, while still
behaving "as if" there were lots of copies. Most compilers (even C
compilers) don't return large structures on the stack or in registers,
but use a hidden reference argument, so most compilers elide the
copies. See RVO and NRVO.

Of course, if you implement operator+=, you can elide those copies
yourself.
Go ahead, throw an exception. Throw it in a templated
constructor.

OK. I will.
Feel the madness. :)
Non-sequitor.

Whoop-de-do. I've seen literally thousands of complaints about almost
everything. People like complaining. Can you name me a language
about which vociferous angry people do not frequently complain?
Have you tried any of the above in practice?
Yup

Some non-trivial project with more than a pair of developers?
Yup

I'm sure that if you did, you'd find it pretty daunting.

Not massively. You're not obliged to use every feature of the
language, and if you try to in C++, it will drive you insane. If
you're talking about doing heavily-templated-metaprogramming without
understanding RVO, you might be trying to run before you can walk. But
good design, a bit of care and a lot of restraint will see you through.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
474,432
Messages
2,571,680
Members
48,796
Latest member
Greg L.

Latest Threads

Top