Exceptions vs Status codes

Discussion in 'C++' started by Gianni Mariani, May 17, 2004.

  1. I'm involved in a new project and a new member on the team has voiced a
    strong opinion that we should utilize exceptions.

    The other members on the team indicate that they have either been burned
    with unmaintainable code (an so are now not using exceptions). My
    position is that "I can be convinced to use exceptions" and my
    experience was that it let to code that was (much) more difficult to debug.

    The team decided that we'd give exceptions a go. Yeah for progress !

    I've been rattling my tiny brain on the issue and I have come to the
    conclusion that exceptions are not as flexible as status codes.

    a) For debugging purposes we want stack traces - but only for cases
    where the exception is caught in a place we consider a "bad" thing happened.

    For example, say we have a "file" object that fails to "open" the file.
    In most cases, it's ok, we don't really care, it's really not
    exceptional except if it's an "important" file. I really don't know if
    it's important at the lower levels (where the exception is thrown).

    I'm ok with psuedo stack trace e.g.


    T func( T0 arg0, T1 arg1 ... )
    try
    {
    ... func magic ...
    }
    catch ( ... )
    {
    LOG( "func", arg0, arg1 ... ); //etc
    throw; // rethrow
    }


    This does not look too bad but it's not foolproof because no-one knows
    which of the arguments actually make sense to log at any point in time.
    I suspect there are also optimizer hits because the values need to be
    stashed away "in case" of exception while in regular code we would be
    logging the arguments like:


    if ( ! func( v1, v2, v3 ) )
    {
    LOG( WARNING, "func() Failed", v1, v2, v3 ... );
    return falase;
    }

    And hence only if "WARNING" level logging is turned on do the values
    need to get stashed.

    b) It would really be nice to know what is going to catch an exception.
    I know this is a bit of a wild thought but it would be nice if there
    was a low-cost was of throwing context dependant exceptions. I suppose
    we could do that using a TSS variable if we had co-operating
    caller/thrower semantics.

    I know this is a contentious topic, I really don't want to start another
    exception war, but I'd really like to hear from people experienced in
    using exceptions in big complex projects and the things to avoid and
    techniques to use to get the best bang for sweat.

    The usual rules are clear.
    1. assert for logic errors - absolutely no exceptions used to detect
    logic errors.
    2. exceptions should occur infrequently in "normal" running of the code.
    3. RAII everywhere etc.

    I'm more interested in fine tuning.

    G
    Gianni Mariani, May 17, 2004
    #1
    1. Advertising

  2. * Gianni Mariani <> schriebt:
    >
    > I'm involved in a new project and a new member on the team has voiced a
    > strong opinion that we should utilize exceptions.
    >
    > The other members on the team indicate that they have either been burned
    > with unmaintainable code (an so are now not using exceptions). My
    > position is that "I can be convinced to use exceptions" and my
    > experience was that it let to code that was (much) more difficult to debug.
    >
    > The team decided that we'd give exceptions a go. Yeah for progress !


    Although strictly off-topic, it would be interesting to know how a team
    where all but one favored not using exceptions, came to decide that they
    should use exceptions.



    > I've been rattling my tiny brain on the issue and I have come to the
    > conclusion that exceptions are not as flexible as status codes.


    The comparision is meaningless.

    Do not replace status codes by exceptions.

    Or vice versa.



    > a) For debugging purposes we want stack traces


    C++ as a language has no facilities for obtaining stack trace information,
    but nearly all tool-sets have.


    > - but only for cases
    > where the exception is caught in a place we consider a "bad" thing happened.


    Unclear.



    > For example, say we have a "file" object that fails to "open" the file.
    > In most cases, it's ok, we don't really care, it's really not
    > exceptional except if it's an "important" file. I really don't know if
    > it's important at the lower levels (where the exception is thrown).


    Do not use exceptions to indicate that a file open failed.

    It's not a breach of contract.

    It's _normal_ and _expected_ behavior that a file open fails.

    An exception does not indicate failure.

    It indicates breach of (normal case) contract: that a function wasn't able
    to do whatever it was designed and contractually obligated to do.



    > b) It would really be nice to know what is going to catch an exception.
    > I know this is a bit of a wild thought but it would be nice if there
    > was a low-cost was of throwing context dependant exceptions. I suppose
    > we could do that using a TSS variable if we had co-operating
    > caller/thrower semantics.


    The language C has a facility like the one you dream of. It's called
    'longjmp'. However, that's not supported in C++ except when using C++
    as C -- e.g. no stack based objects that need to be destroyed.

    But why would you _want_ such spaghetti?

    --
    A: Because it messes up the order in which people normally read text.
    Q: Why is top-posting such a bad thing?
    A: Top-posting.
    Q: What is the most annoying thing on usenet and in e-mail?
    Alf P. Steinbach, May 17, 2004
    #2
    1. Advertising

  3. Gianni Mariani

    Ian Guest

    Gianni Mariani wrote:

    <snip>
    > I know this is a contentious topic, I really don't want to start another
    > exception war, but I'd really like to hear from people experienced in
    > using exceptions in big complex projects and the things to avoid and
    > techniques to use to get the best bang for sweat.
    >
    > The usual rules are clear.
    > 1. assert for logic errors - absolutely no exceptions used to detect
    > logic errors.
    > 2. exceptions should occur infrequently in "normal" running of the code.
    > 3. RAII everywhere etc.
    >

    One bit of advice I'd like to offer is to have a base exception class
    that has file, line and text members. This covers most of the
    objections. You know where the exception was thrown and why.

    Your exception base constructor can also double as your logger.

    I tend to use a macro to throw such an exception,

    #define Whinge( info ) throw Exception( (info), __FILE__, __LINE__ )

    You can use this to make a 'soft assert'.

    Use the exception's what() member to print out the details if required.
    Very handy if you use a unit test framework like cppunit.

    Ian
    Ian, May 17, 2004
    #3
  4. Gianni Mariani

    JKop Guest

    Alf P. Steinbach posted:

    > * Gianni Mariani <> schriebt:
    >>
    >> I'm involved in a new project and a new member on the team has voiced
    >> a strong opinion that we should utilize exceptions.
    >>
    >> The other members on the team indicate that they have either been
    >> burned with unmaintainable code (an so are now not using exceptions).
    >> My position is that "I can be convinced to use exceptions" and my
    >> experience was that it let to code that was (much) more difficult to
    >> debug.
    >>
    >> The team decided that we'd give exceptions a go. Yeah for progress !

    >
    > Although strictly off-topic, it would be interesting to know how a team
    > where all but one favored not using exceptions, came to decide that
    > they should use exceptions.



    I disagree: If topical on-topic C++ conversation leads to a team's
    preference for exceptions, then it *is* topical and hence, *on*-topic.


    -JKop
    JKop, May 17, 2004
    #4
  5. Alf P. Steinbach wrote:
    > * Gianni Mariani <> schriebt:
    >
    >>I'm involved in a new project and a new member on the team has voiced a
    >>strong opinion that we should utilize exceptions.
    >>
    >>The other members on the team indicate that they have either been burned
    >>with unmaintainable code (an so are now not using exceptions). My
    >>position is that "I can be convinced to use exceptions" and my
    >>experience was that it let to code that was (much) more difficult to debug.
    >>
    >>The team decided that we'd give exceptions a go. Yeah for progress !

    >
    >
    > Although strictly off-topic, it would be interesting to know how a team
    > where all but one favored not using exceptions, came to decide that they
    > should use exceptions.


    It's a small team - 1 person is 20% of the team ! I think I was the
    swing vote and the other hold-out was promised a number of things to
    avoid issues he didn't like.

    Many years ago I avoided exceptions because there were too many issues
    with compilers' support. Since then I have seen so much unmaintainable
    code with exceptions that I simply was not convinced. The new team
    member seems to have a different experience and was able to convince me
    that there is a way to do it right.


    >
    >>I've been rattling my tiny brain on the issue and I have come to the
    >>conclusion that exceptions are not as flexible as status codes.

    >
    >
    > The comparision is meaningless.
    >
    > Do not replace status codes by exceptions.
    >
    > Or vice versa.


    Everything you can do with exceptions you can do with status codes and
    visa versa (given that you are writing the code that is).

    The argument is that :


    if ( ! DoSomthing() )
    {
    LOG( somthing bad happened );
    }

    is equivalent to

    try { DoSomthing(); } catch ( X & x ) { LOG( somthing bad happened ); }

    but with exceptons, I can reduce the complexity of the code:

    try {
    DoSomthing();
    DoSomthing();
    DoSomthing();
    } ...

    or more to the point you can use return values and keep the code simple.

    try {
    DoSomthing( File( "X.file" ) ) = BlastBytes();
    }....

    Nice, easy to read code that stops in it's tracks is "somthing bad happens".

    >
    >>a) For debugging purposes we want stack traces

    >
    >
    > C++ as a language has no facilities for obtaining stack trace information,
    > but nearly all tool-sets have.


    >
    >
    >
    >>- but only for cases
    >>where the exception is caught in a place we consider a "bad" thing happened.

    >
    >
    > Unclear.


    That's the point.

    >
    >
    >>For example, say we have a "file" object that fails to "open" the file.
    >>In most cases, it's ok, we don't really care, it's really not
    >>exceptional except if it's an "important" file. I really don't know if
    >>it's important at the lower levels (where the exception is thrown).

    >
    >
    > Do not use exceptions to indicate that a file open failed.


    I think this *depends* on the use of the file object.

    >
    > It's not a breach of contract.
    >
    > It's _normal_ and _expected_ behavior that a file open fails.


    What is normal is context dependant. For example, if a configuration
    file is *required* for the code to run, it is an "exceptional" situation
    while if a user requested file does not exist, well that's user error
    which (IMHO) should not be an exception.

    >
    > An exception does not indicate failure.
    >
    > It indicates breach of (normal case) contract: that a function wasn't able
    > to do whatever it was designed and contractually obligated to do.


    So there are:

    a) breach of contract - hard assert
    b) breach of *normal* contract - exception

    So one has to define what *normal* contract is ? I have no particular
    exception [:)] to this idea but it does require documentation.

    >
    >>b) It would really be nice to know what is going to catch an exception.
    >> I know this is a bit of a wild thought but it would be nice if there
    >>was a low-cost was of throwing context dependant exceptions. I suppose
    >>we could do that using a TSS variable if we had co-operating
    >>caller/thrower semantics.

    >
    >
    > The language C has a facility like the one you dream of. It's called
    > 'longjmp'. However, that's not supported in C++ except when using C++
    > as C -- e.g. no stack based objects that need to be destroyed.
    >
    > But why would you _want_ such spaghetti?


    Actually, you could do this the C++ way with a few simple C++ mechanism's.
    Gianni Mariani, May 17, 2004
    #5
  6. Ian wrote:
    > Gianni Mariani wrote:
    >
    > <snip>
    >
    >> I know this is a contentious topic, I really don't want to start
    >> another exception war, but I'd really like to hear from people
    >> experienced in using exceptions in big complex projects and the things
    >> to avoid and techniques to use to get the best bang for sweat.
    >>
    >> The usual rules are clear.
    >> 1. assert for logic errors - absolutely no exceptions used to detect
    >> logic errors.
    >> 2. exceptions should occur infrequently in "normal" running of the code.
    >> 3. RAII everywhere etc.
    >>

    > One bit of advice I'd like to offer is to have a base exception class
    > that has file, line and text members. This covers most of the
    > objections. You know where the exception was thrown and why.
    >
    > Your exception base constructor can also double as your logger.
    >
    > I tend to use a macro to throw such an exception,
    >
    > #define Whinge( info ) throw Exception( (info), __FILE__, __LINE__ )
    >
    > You can use this to make a 'soft assert'.
    >
    > Use the exception's what() member to print out the details if required.
    > Very handy if you use a unit test framework like cppunit.


    Yep, the current "assert" mechanism we use will normally do a hard
    assert unless it's running in a test case in which case it does a "test
    case assert" which actually throws a "TestCase" exception.
    Gianni Mariani, May 17, 2004
    #6
  7. * Gianni Mariani <> schriebt:
    >
    > Everything you can do with exceptions you can do with status codes and
    > visa versa (given that you are writing the code that is).


    Only by using exceptions as non-local dynamic goto's. Goto is bad enough.
    Non-local dynamic goto's are infinitely worse.


    > The argument is that :
    >
    >
    > if ( ! DoSomthing() )
    > {
    > LOG( somthing bad happened );
    > }
    >
    > is equivalent to
    >
    > try { DoSomthing(); } catch ( X & x ) { LOG( somthing bad happened ); }


    No it isn't (and btw., don't catch exceptions by reference to non-const).
    Most obvious, the latter is typically much less efficient. Perhaps less
    obvious, the latter can invoke std::terminate where the former does not.
    But most important, the latter implementation of DoSomthing has a different
    contract the former. The bool-returning version has an obligation to try
    to achieve something but does not guarantee it; the exception-throwing
    version does guarantee the result, and client code can be written as if
    that guarantee holds -- without catching or caring about the exception
    (whereas with the status-code version client code must check the status).


    > but with exceptons, I can reduce the complexity of the code:
    >
    > try {
    > DoSomthing();
    > DoSomthing();
    > DoSomthing();
    > } ...


    That's incorrect. You have reduced the complexity of one particular
    instance of client code. If it's natural for DoSomthing to return
    false then you have also increased complexity of other client code.



    > or more to the point you can use return values and keep the code simple.
    >
    > try {
    > DoSomthing( File( "X.file" ) ) = BlastBytes();
    > }....
    >
    > Nice, easy to read code


    I'm almost throwing up.



    > > Do not use exceptions to indicate that a file open failed.

    >
    > I think this *depends* on the use of the file object.


    It does.


    > What is normal is context dependant.


    It is, but not in the sense you seem to mean.

    What is normal for a given function Foo is not context dependent.

    But you can use Foo as part of the implementation of code that offers
    a different contract than Foo does, e.g. Foo is OpenFile, and the wrapper
    code with a somewhat stronger contract is OpenConfigurationFile.

    --
    A: Because it messes up the order in which people normally read text.
    Q: Why is top-posting such a bad thing?
    A: Top-posting.
    Q: What is the most annoying thing on usenet and in e-mail?
    Alf P. Steinbach, May 17, 2004
    #7
  8. Alf P. Steinbach wrote:
    > * Gianni Mariani <> schriebt:
    >
    >>Everything you can do with exceptions you can do with status codes and
    >>visa versa (given that you are writing the code that is).

    >
    >
    > Only by using exceptions as non-local dynamic goto's. Goto is bad enough.
    > Non-local dynamic goto's are infinitely worse.


    I agree with the latter but I don't understand *your* definition of
    non-local dynamic goto. In essance any use of exceptions is equivalent
    to a non-local dynamic goto.

    >
    >>The argument is that :
    >>
    >>
    >>if ( ! DoSomthing() )
    >>{
    >> LOG( somthing bad happened );
    >>}
    >>
    >>is equivalent to
    >>
    >>try { DoSomthing(); } catch ( X & x ) { LOG( somthing bad happened ); }

    >
    >
    > No it isn't (and btw., don't catch exceptions by reference to non-const).
    > Most obvious, the latter is typically much less efficient.


    Only if DoSomthing throws an exception, otherwise in theory it is much
    more efficient, right ?

    Perhaps less
    > obvious, the latter can invoke std::terminate where the former does not.


    std:terminate can be called for all kinds of reasons beyond what happens
    here.

    > But most important, the latter implementation of DoSomthing has a different
    > contract the former. The bool-returning version has an obligation to try
    > to achieve something but does not guarantee it; the exception-throwing
    > version does guarantee the result, and client code can be written as if
    > that guarantee holds -- without catching or caring about the exception
    > (whereas with the status-code version client code must check the status).


    Which means, *in theory*, you can write simpler-looking code (or
    deceptively simpler-looking might be better).

    >
    >>but with exceptons, I can reduce the complexity of the code:
    >>
    >>try {
    >> DoSomthing();
    >> DoSomthing();
    >> DoSomthing();
    >>} ...

    >
    >
    > That's incorrect. You have reduced the complexity of one particular
    > instance of client code. If it's natural for DoSomthing to return
    > false then you have also increased complexity of other client code.


    It depends on what "natural" means.

    >
    >
    >
    >
    >>or more to the point you can use return values and keep the code simple.
    >>
    >> try {
    >> DoSomthing( File( "X.file" ) ) = BlastBytes();
    >> }....
    >>
    >>Nice, easy to read code

    >
    >
    > I'm almost throwing up.


    Do explain.

    >
    >>>Do not use exceptions to indicate that a file open failed.

    >>
    >>I think this *depends* on the use of the file object.

    >
    >
    > It does.
    >
    >
    >
    >>What is normal is context dependant.

    >
    >
    > It is, but not in the sense you seem to mean.
    >
    > What is normal for a given function Foo is not context dependent.
    >
    > But you can use Foo as part of the implementation of code that offers
    > a different contract than Foo does, e.g. Foo is OpenFile, and the wrapper
    > code with a somewhat stronger contract is OpenConfigurationFile.


    OK, this I get.

    This infers I need a different class for each kind of context a concept
    may be used in with respect to exceptions. This can have a significant
    impact on code complexity.
    Gianni Mariani, May 17, 2004
    #8
  9. * Gianni Mariani <> schriebt:
    > Alf P. Steinbach wrote:
    > > * Gianni Mariani <> schriebt:
    > >
    > >>Everything you can do with exceptions you can do with status codes and
    > >>visa versa (given that you are writing the code that is).

    > >
    > >
    > > Only by using exceptions as non-local dynamic goto's. Goto is bad enough.
    > > Non-local dynamic goto's are infinitely worse.

    >
    > I agree with the latter but I don't understand *your* definition of
    > non-local dynamic goto. In essance any use of exceptions is equivalent
    > to a non-local dynamic goto.


    Well, that's my definition... ;-)

    The difference is whether you care or not where an exception will be caught.

    Exception are beneficial when you don't care about where they'll end up.

    Used that way it makes no difference that internally they do some goto'ing;
    just like it makes no difference that a 'for'-loop internally does some
    goto'ing.

    But when you do care, at the point of throwing, where an exception will end
    up, then you're _using_ the exception as a goto.

    And that has some negative consequences.

    In the case of the boolean function that is rewritten as a void function
    throwing an exception, that function then takes partial responsibility for
    choosing one of two paths in the client code, and to do that it requires the
    client code to use a try-catch, and it's then much more diffult to ignore the
    logical 'false' result (an exception) should that be desired, and also it's
    then much more difficult to write correct client code, because there may be
    other possible exceptions, and it's much more difficult to test.



    > >>The argument is that :
    > >>
    > >>
    > >>if ( ! DoSomthing() )
    > >>{
    > >> LOG( somthing bad happened );
    > >>}
    > >>
    > >>is equivalent to
    > >>
    > >>try { DoSomthing(); } catch ( X & x ) { LOG( somthing bad happened ); }

    > >
    > >
    > > No it isn't (and btw., don't catch exceptions by reference to non-const).
    > > Most obvious, the latter is typically much less efficient.

    >
    > Only if DoSomthing throws an exception, otherwise in theory it is much
    > more efficient, right ?


    Nope, but neither is it necessarily much less efficient in the case of no
    exception.

    Some C++ implementations such as Visual C++ has some overhead even in this
    case, in order to support low-level (non-C++) exceptions.

    But focusing on local overhead or not in the context of clarity and robustness
    is nearly always the wrong thing to do (i.e. leading to bad decisions). Local
    efficiency can be improved by coding in e.g. assemler language. We don't do
    that, for the time that is used on coding in assembler language can be much
    more profitably spent on correctness, clarity and high-level optimization in
    C++ -- and using exceptions is very much about correctness and clarity.


    > Perhaps less
    > > obvious, the latter can invoke std::terminate where the former does not.

    >
    > std:terminate can be called for all kinds of reasons beyond what happens
    > here.


    Yes, but my point is that one must be very careful when thinking about
    equivalence-preserving transformations of code.

    There is no transformation that is guaranteed to yield 100% equivalent code.

    Some things change, if only the line numbering (or whatever); and in the case
    of transforming from status code to exception contracts change, efficiency in
    different situtations changes, the possibility of calls to std::terminate
    (e.g. if an automatic destructor call throws) changes, and so on; in
    particular, how to test the code and how it can be tested changes _a lot_.


    > >>
    > >> try {
    > >> DoSomthing( File( "X.file" ) ) = BlastBytes();
    > >> }....
    > >>
    > >>Nice, easy to read code

    > >
    > >
    > > I'm almost throwing up.

    >
    > Do explain.


    The File object is destroyed before the assignment. What does DoSomething
    return? It's not at all nice code, but in fact incomprehensible code. That,
    however, has nothing to do with use of exceptions or not. AFAICS.


    > > What is normal for a given function Foo is not context dependent.
    > >
    > > But you can use Foo as part of the implementation of code that offers
    > > a different contract than Foo does, e.g. Foo is OpenFile, and the wrapper
    > > code with a somewhat stronger contract is OpenConfigurationFile.

    >
    > OK, this I get.
    >
    > This infers I need a different class for each kind of context a concept
    > may be used in with respect to exceptions. This can have a significant
    > impact on code complexity.


    Not necessarily a different class, but at least some simple function wrappers.

    And yes, that does tend to reduce code complexity.

    It reduces complexity because it replaces implied, context-dependent contracts
    by explicit, static contracts, which is nearly always a Good Thing (TM).

    Implied contracts have to be figured out.

    And in maintainance coding, which constitutes the bulk of coding, implied,
    context-dependent contracts are seldom fully understood (let's see, if I
    change a little here and copy some old working code from another context in
    here, yep, it seems to work! hurray!), which means they're not adhered to as
    they should, which means bugs and complexity.

    --
    A: Because it messes up the order in which people normally read text.
    Q: Why is top-posting such a bad thing?
    A: Top-posting.
    Q: What is the most annoying thing on usenet and in e-mail?
    Alf P. Steinbach, May 18, 2004
    #9
  10. Alf P. Steinbach wrote:
    > * Gianni Mariani <> schriebt:


    ....

    > logical 'false' result (an exception) should that be desired, and also it's
    > then much more difficult to write correct client code, because there may be
    > other possible exceptions, and it's much more difficult to test.
    >


    Agreed.

    .... snipped lots of stuff ...
    >
    >
    >
    >>>> try {
    >>>> DoSomthing( File( "X.file" ) ) = BlastBytes();
    >>>> }....
    >>>>
    >>>>Nice, easy to read code
    >>>
    >>>
    >>>I'm almost throwing up.

    >>
    >>Do explain.

    >
    >
    > The File object is destroyed before the assignment. What does DoSomething
    > return? It's not at all nice code, but in fact incomprehensible code. That,
    > however, has nothing to do with use of exceptions or not. AFAICS.


    File is not destroyed until the expression is evaluated. DoSomthing
    *may* return a proxy or it may be a badly named class ! The point was
    that if the File constructor failed by throwing an exception, magically
    things would do the Right(TM) thing.

    >
    >
    >>>What is normal for a given function Foo is not context dependent.
    >>>
    >>>But you can use Foo as part of the implementation of code that offers
    >>>a different contract than Foo does, e.g. Foo is OpenFile, and the wrapper
    >>>code with a somewhat stronger contract is OpenConfigurationFile.

    >>
    >>OK, this I get.
    >>
    >>This infers I need a different class for each kind of context a concept
    >>may be used in with respect to exceptions. This can have a significant
    >>impact on code complexity.

    >
    >
    > Not necessarily a different class, but at least some simple function wrappers.
    >
    > And yes, that does tend to reduce code complexity.
    >
    > It reduces complexity because it replaces implied, context-dependent contracts
    > by explicit, static contracts, which is nearly always a Good Thing (TM).
    >
    > Implied contracts have to be figured out.
    >
    > And in maintainance coding, which constitutes the bulk of coding, implied,
    > context-dependent contracts are seldom fully understood (let's see, if I
    > change a little here and copy some old working code from another context in
    > here, yep, it seems to work! hurray!), which means they're not adhered to as
    > they should, which means bugs and complexity.


    I was thinking that it would add code complexity ! OK, I'll soon find out !
    Gianni Mariani, May 18, 2004
    #10
    1. Advertising

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

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. Andy Fish

    constants for http status codes

    Andy Fish, Mar 3, 2005, in forum: ASP .Net
    Replies:
    6
    Views:
    3,782
    Juan T. Llibre
    Mar 4, 2005
  2. Greg  --
    Replies:
    4
    Views:
    2,137
  3. Replies:
    2
    Views:
    2,798
    Malcolm
    Aug 20, 2005
  4. Allen
    Replies:
    1
    Views:
    627
    Mark Rae [MVP]
    Dec 3, 2007
  5. chen

    Architecture question: Exceptions vs status codes

    chen, Dec 20, 2005, in forum: ASP .Net Web Services
    Replies:
    7
    Views:
    199
Loading...

Share This Page